diff --git a/dag/extdeps/filesystem/filesystem_io.dag b/dag/extdeps/filesystem/filesystem_io.dag index 645669553f8..90ca08e28a7 100644 --- a/dag/extdeps/filesystem/filesystem_io.dag +++ b/dag/extdeps/filesystem/filesystem_io.dag @@ -722,6 +722,7 @@ service Filesystem { entries: String from "entries" success: Bool from "list_success" error: String from "error" + error_kind: String from "error_kind" } readonly transport file { path: "{path}", verb: "list" } diff --git a/dag/extdeps/google/issue_tracker.dag b/dag/extdeps/google/issue_tracker.dag index cfe7ee50705..5dbf6c87604 100644 --- a/dag/extdeps/google/issue_tracker.dag +++ b/dag/extdeps/google/issue_tracker.dag @@ -545,45 +545,6 @@ type IssueActionBar { actions: List } -// THE FLEET COMPONENT TREE (owner directive 2026-09-24: srvN as the component/asset hierarchy so -// fleet issues carry a real component placement). SOURCES, stated honestly: the host roster and -// the group placement are the fleet's own modeled facts — srv1..srv4 the runner fleet, srv5..srv12 -// the Sparks, and the serving arms per the fabric observation's cages (cage 1 = group A on -// srv5..srv8, cage 2 = group B on srv9..srv12; gunbc.spark.fabric_switch_observed -// fabric_group_hosts) — carried here as OWNER-DIRECTED interface rows citing the fleet modules, -// not re-derived at read time. Component ids are stable table keys, not derived from the path. -data fleet_component_hosts: List = [ - Component { id: 1, path: ["srv1"] }, - Component { id: 2, path: ["srv2"] }, - Component { id: 3, path: ["srv3"] }, - Component { id: 4, path: ["srv4"] }, - Component { id: 5, path: ["srv5"] }, - Component { id: 6, path: ["srv6"] }, - Component { id: 7, path: ["srv7"] }, - Component { id: 8, path: ["srv8"] }, - Component { id: 9, path: ["srv9"] }, - Component { id: 10, path: ["srv10"] }, - Component { id: 11, path: ["srv11"] }, - Component { id: 12, path: ["srv12"] }, -] - -// THE SERVING-ARM SUB-COMPONENTS: each Spark host carries its serving arm as a sub-component -// (path = host path + the arm), so an issue's component placement names the arm it touches. -data fleet_component_serving_arms: List = [ - Component { id: 105, path: ["srv5", "serving", "group-a"] }, - Component { id: 106, path: ["srv6", "serving", "group-a"] }, - Component { id: 107, path: ["srv7", "serving", "group-a"] }, - Component { id: 108, path: ["srv8", "serving", "group-a"] }, - Component { id: 109, path: ["srv9", "serving", "group-b"] }, - Component { id: 110, path: ["srv10", "serving", "group-b"] }, - Component { id: 111, path: ["srv11", "serving", "group-b"] }, - Component { id: 112, path: ["srv12", "serving", "group-b"] }, -] - -fn fleet_components() -> List { - concat(fleet_component_hosts, fleet_component_serving_arms) -} - // A COMPONENT'S PLACEMENT IS ITS PATH (the type's own law): the browse tree derives from the // paths, never a second spelling. "Browse components" is one of the four navigation/curation // sidebar surfaces (stock_sidebar_roster) — the tree it renders is THIS derivation over the diff --git a/dag/extdeps/google/workspace.dag b/dag/extdeps/google/workspace.dag index 1ef1e6478b0..9bf721794b0 100644 --- a/dag/extdeps/google/workspace.dag +++ b/dag/extdeps/google/workspace.dag @@ -1,6 +1,6 @@ module extdeps.google.workspace -import std.types { NonEmptyStr } +import std.types { NonEmptyStr, String, Bool, List } import std.decl_ref { DeclarationRef, WholeDeclaration } import extdeps.external_authority { ExternalAuthority, ExternalModelScope, ExternalSubjectRef } import extdeps.uri { Uri, Https } @@ -32,3 +32,96 @@ data extdeps_model_scope: ExternalModelScope = ExternalModelScope { first_citation: extdeps_external_authority_anchor, further_citations: [google_workspace_credentials_citation] } + +// Reviewed 2026-09-28 against Google's Auth Platform, Admin console and Policy API documentation. +// API availability is per setting and operation. A directory/user/photo API is not an API for +// its governing policy. IAM oauthClients is Workforce Identity Federation, not Auth Platform. + +type WorkspaceAdminControl + = OAuthClientRegistration + | OAuthApplicationAudience + | OAuthClientAppAccess + | ExternalDirectorySharing + | ProfilePhotoEditing + +type WorkspaceAdminSubject + = AuthPlatformClient { project: NonEmptyStr, client: NonEmptyStr } + | AuthPlatformAudience { project: NonEmptyStr } + | WorkspaceClientAccess { customer: NonEmptyStr, client: NonEmptyStr, org_unit: NonEmptyStr } + | WorkspaceDirectorySharing { customer: NonEmptyStr } + | WorkspacePhotoEditing { customer: NonEmptyStr, org_unit: NonEmptyStr } + +type WorkspaceAppAccess = AppTrusted | AppLimited | AppBlocked | AppSpecificGoogleData { scopes: List } +type WorkspaceDirectorySharingPolicy = RequesterBasicProfileOnly | OrganizationDirectoryData + +type WorkspaceAdminValue + = RegisteredWebClient { redirect_uris: List } + | InternalAudience + | ExternalAudience + | ClientAccess { access: WorkspaceAppAccess } + | DirectorySharing { policy: WorkspaceDirectorySharingPolicy } + | PhotoEditing { allowed: Bool } + +type WorkspaceAdminReadSurface + = ConsoleReadbackRequired { documentation: NonEmptyStr } + | PolicyApiReadSupported { setting_type: NonEmptyStr, documentation: NonEmptyStr } + +type WorkspaceAdminWriteSurface = ManualConsoleAdministration { documentation: NonEmptyStr } + +data workspace_policy_settings_documentation: NonEmptyStr = "https://docs.cloud.google.com/identity/docs/concepts/supported-policy-api-settings" +data workspace_policy_read_documentation: NonEmptyStr = "https://docs.cloud.google.com/identity/docs/how-to/list-get-policies" +data workspace_policy_readonly_scope: NonEmptyStr = "https://www.googleapis.com/auth/cloud-identity.policies.readonly" +data workspace_policy_list_endpoint: NonEmptyStr = "https://cloudidentity.googleapis.com/v1/policies" +data workspace_policy_reduction_documentation: NonEmptyStr = "https://docs.cloud.google.com/identity/docs/concepts/policy-api-concepts" + +fn workspace_admin_control(subject: WorkspaceAdminSubject) -> WorkspaceAdminControl { + match subject { + AuthPlatformClient { project: _, client: _ } => OAuthClientRegistration + AuthPlatformAudience { project: _ } => OAuthApplicationAudience + WorkspaceClientAccess { customer: _, client: _, org_unit: _ } => OAuthClientAppAccess + WorkspaceDirectorySharing { customer: _ } => ExternalDirectorySharing + WorkspacePhotoEditing { customer: _, org_unit: _ } => ProfilePhotoEditing + } +} + +fn workspace_admin_value_control(value: WorkspaceAdminValue) -> WorkspaceAdminControl { + match value { + RegisteredWebClient { redirect_uris: _ } => OAuthClientRegistration + InternalAudience => OAuthApplicationAudience + ExternalAudience => OAuthApplicationAudience + ClientAccess { access: _ } => OAuthClientAppAccess + DirectorySharing { policy: _ } => ExternalDirectorySharing + PhotoEditing { allowed: _ } => ProfilePhotoEditing + } +} + +fn workspace_admin_console_documentation(control: WorkspaceAdminControl) -> NonEmptyStr { + match control { + OAuthClientRegistration => "https://support.google.com/cloud/answer/15549257" + OAuthApplicationAudience => "https://developers.google.com/workspace/guides/configure-oauth-consent" + OAuthClientAppAccess => "https://support.google.com/a/answer/7281227" + ExternalDirectorySharing => "https://knowledge.workspace.google.com/admin/users/let-third-party-apps-access-directory-data" + ProfilePhotoEditing => "https://knowledge.workspace.google.com/admin/users/allow-directory-users-to-change-their-profile-and-photo" + } +} + +// No documented supported read for rows 1, 2, 3 or 5 in the reviewed interface inventories. +// This is a dated interface qualification, not a claim that Google can never expose one. +fn workspace_admin_read_surface(control: WorkspaceAdminControl) -> WorkspaceAdminReadSurface { + match control { + ExternalDirectorySharing => PolicyApiReadSupported { setting_type: "directory.external_directory_sharing", documentation: workspace_policy_settings_documentation } + _ => ConsoleReadbackRequired { documentation: workspace_admin_console_documentation(control: control) } + } +} + +fn workspace_admin_write_surface(control: WorkspaceAdminControl) -> WorkspaceAdminWriteSurface { + ManualConsoleAdministration { documentation: workspace_admin_console_documentation(control: control) } +} + +// The literal policy enum is distinct from absence/refusal; an omitted/default setting needs +// the provider's policy reduction/default rules, never an assumption based on a successful login. +fn workspace_directory_sharing_value(wire: String) -> WorkspaceDirectorySharingPolicy? { + if wire == "REQUESTER_BASIC_PROFILE_ONLY" { Present { value: RequesterBasicProfileOnly } } + else if wire == "ORGANIZATION_DIRECTORY_DATA" { Present { value: OrganizationDirectoryData } } + else { none } +} diff --git a/dag/extdeps/http/form_urlencoded.dag b/dag/extdeps/http/form_urlencoded.dag new file mode 100644 index 00000000000..b961379f654 --- /dev/null +++ b/dag/extdeps/http/form_urlencoded.dag @@ -0,0 +1,81 @@ +module extdeps.http.form_urlencoded + +import std.types { String, List, Bool, Int } +import std.integer { UInt8 } +import std.bytes { utf8_encode_bytes, bytes_octets } +import std.encoding { utf8_decode_octets } + +// WHATWG URL, application/x-www-form-urlencoded: split pairs before decoding, plus means space, +// percent escapes decode exactly once. This admission refuses malformed escapes/UTF-8 and duplicate +// fields rather than selecting an ambiguous command. https://url.spec.whatwg.org/#urlencoded-parsing +// Empty and absent fields remain distinct. CRLF and field-looking lines inside values are data. +type FormField { name: String, value: String } +type FormRead = FormDecoded { fields: List } | FormRefused { reason: String } +type FormOctets = FormOctetsDecoded { octets: List } | FormOctetsRefused + +fn form_hex(o: UInt8) -> Int? { + if o >= 48 && o <= 57 { Present { value: (o as Int) - 48 } } + else if o >= 65 && o <= 70 { Present { value: (o as Int) - 55 } } + else if o >= 97 && o <= 102 { Present { value: (o as Int) - 87 } } + else { none } +} + +fn form_unescape(octets: List, acc: List) -> FormOctets { + match octets.first() { + Absent => FormOctetsDecoded { octets: acc } + Present { value: head } => if head == 43 { form_unescape(octets: octets.skip(n: 1), acc: concat(acc, [32])) } + else if head != 37 { form_unescape(octets: octets.skip(n: 1), acc: concat(acc, [head])) } + else { match octets.skip(n: 1).first() { + Absent => FormOctetsRefused + Present { value: h } => match octets.skip(n: 2).first() { + Absent => FormOctetsRefused + Present { value: l } => match form_hex(o: h) { + Absent => FormOctetsRefused + Present { value: high } => match form_hex(o: l) { + Absent => FormOctetsRefused + Present { value: low } => form_unescape(octets: octets.skip(n: 3), acc: concat(acc, [high * 16 + low])) + } + } + } + } } + } +} + +fn form_component(wire: String) -> String? { + match form_unescape(octets: bytes_octets(b: utf8_encode_bytes(s: wire)), acc: []) { + FormOctetsRefused => none + FormOctetsDecoded { octets } => match utf8_decode_octets(octets: octets) { + Absent => none + Present { value: scalars } => Present { value: join(map(scalars, cp => from_code_point(cp: cp)), "") } + } + } +} + +fn form_decode(wire: String) -> FormRead { + fold(filter(split(s: wire, delimiter: "&"), part => part != ""), init: FormDecoded { fields: [] }, f: (state, part) => match state { + FormRefused { reason } => FormRefused { reason: reason } + FormDecoded { fields } => { + let pieces = split(s: part, delimiter: "=") + let name_wire = match pieces.first() { Present { value: first } => first Absent => "" } + let value_wire = join(pieces.skip(n: 1), "=") + match form_component(wire: name_wire) { + Absent => FormRefused { reason: "invalid encoded form name" } + Present { value: name } => if any(fields, field => field.name == name) { FormRefused { reason: "duplicate form field" } } + else { match form_component(wire: value_wire) { + Absent => FormRefused { reason: "invalid encoded form value" } + Present { value } => FormDecoded { fields: concat(fields, [FormField { name: name, value: value }]) } + } } + } + } + }) +} + +fn form_field(form: FormRead, name: String) -> String? { + match form { + FormRefused { reason: _ } => none + FormDecoded { fields } => match filter(fields, field => field.name == name).first() { + Absent => none + Present { value: field } => Present { value: field.value } + } + } +} diff --git a/dag/gunbc/auth/google_workspace_admin_convergence.dag b/dag/gunbc/auth/google_workspace_admin_convergence.dag new file mode 100644 index 00000000000..1c655e7eff6 --- /dev/null +++ b/dag/gunbc/auth/google_workspace_admin_convergence.dag @@ -0,0 +1,130 @@ +module gunbc.auth.google_workspace_admin_convergence + +import std.types { String, NonEmptyStr, List, Bool, Int } +import std.goal_assessment { GoalAssessment, GoalSatisfied, GoalAssessmentRefused, GoalInspection, GoalObservationRefused, GoalInspected, assess_by_deviations } +import gunbc.principal_projection { PrincipalRef, principal_ref_eq } +import extdeps.google.workspace { + WorkspaceAdminSubject, WorkspaceAdminValue, WorkspaceAdminControl, WorkspaceAdminReadSurface, + AuthPlatformClient, AuthPlatformAudience, WorkspaceClientAccess, WorkspaceDirectorySharing, WorkspacePhotoEditing, + RegisteredWebClient, InternalAudience, ClientAccess, AppSpecificGoogleData, DirectorySharing, RequesterBasicProfileOnly, OrganizationDirectoryData, PhotoEditing, + workspace_admin_control, workspace_admin_value_control, workspace_admin_read_surface, ConsoleReadbackRequired, PolicyApiReadSupported, +} + +// A manual action is an outstanding obligation. It is never an API mutation or a completed +// convergence merely because a login or a profile fetch succeeded. Values and receipts contain +// no client secret; provisioning that secret uses the existing deployment/Secret Manager path. +type WorkspaceIdentityBinding { + customer: NonEmptyStr + project: NonEmptyStr + client: NonEmptyStr + top_org_unit: NonEmptyStr + redirect_uri: NonEmptyStr + desired_revision: NonEmptyStr +} + +type WorkspaceAdminObligation { + subject: WorkspaceAdminSubject + desired: WorkspaceAdminValue + desired_revision: NonEmptyStr + rationale: NonEmptyStr +} + +type DirectoryDiscoveryPolicy + = BasicSignInOnly + | OrganizationDiscoveryRequired { rationale: NonEmptyStr } + +fn workspace_identity_admin_obligations(binding: WorkspaceIdentityBinding, discovery: DirectoryDiscoveryPolicy) -> List { + [ + WorkspaceAdminObligation { subject: AuthPlatformClient { project: binding.project, client: binding.client }, + desired: RegisteredWebClient { redirect_uris: [binding.redirect_uri] }, desired_revision: binding.desired_revision, rationale: "Register the tracker web client; provision its secret separately through the existing secret path." }, + WorkspaceAdminObligation { subject: AuthPlatformAudience { project: binding.project }, desired: InternalAudience, + desired_revision: binding.desired_revision, rationale: "The tracker is an internal organization application." }, + WorkspaceAdminObligation { subject: WorkspaceClientAccess { customer: binding.customer, client: binding.client, org_unit: binding.top_org_unit }, + desired: ClientAccess { access: AppSpecificGoogleData { scopes: ["openid", "email", "profile"] } }, desired_revision: binding.desired_revision, + rationale: "Admit this client to basic sign-in scopes without granting unrelated Google data." }, + WorkspaceAdminObligation { subject: WorkspaceDirectorySharing { customer: binding.customer }, + desired: DirectorySharing { policy: match discovery { BasicSignInOnly => RequesterBasicProfileOnly OrganizationDiscoveryRequired { rationale: _ } => OrganizationDirectoryData } }, + desired_revision: binding.desired_revision, rationale: match discovery { BasicSignInOnly => "Only the authenticated user's basic profile is required." OrganizationDiscoveryRequired { rationale } => rationale } }, + WorkspaceAdminObligation { subject: WorkspacePhotoEditing { customer: binding.customer, org_unit: binding.top_org_unit }, + desired: PhotoEditing { allowed: true }, desired_revision: binding.desired_revision, + rationale: "Allow profile photo editing under the declared organization policy; read back effective child overrides separately." } + ] +} + +type WorkspaceReadbackSource + = AdminConsoleReadback { administrator: PrincipalRef, evidence: NonEmptyStr } + | WorkspacePolicyApiReadback { policy_resource: NonEmptyStr, setting_type: NonEmptyStr, evidence: NonEmptyStr } + +type WorkspacePolicyCoverage + = EffectiveAtDeclaredScope + | EffectiveWithUnresolvedOverrides { reason: NonEmptyStr } + +type WorkspaceAdminReadback { + subject: WorkspaceAdminSubject + observed: WorkspaceAdminValue + source: WorkspaceReadbackSource + observed_at_epoch: Int + valid_until_epoch: Int + coverage: WorkspacePolicyCoverage +} + +type WorkspaceAdminObservation + = WorkspaceAdminRead { receipt: WorkspaceAdminReadback } + | WorkspaceAdminNotObserved { reason: NonEmptyStr } + | WorkspaceAdminReadRefused { reason: NonEmptyStr } + +type WorkspaceAdminEvidence { desired_revision: NonEmptyStr, receipt: WorkspaceAdminReadback } +type WorkspaceAdminDifference { expected: WorkspaceAdminValue, observed: WorkspaceAdminValue } + +// Receipt admission is subject-bound and freshness-bound. The approver/reader is an existing +// stable principal. A console receipt cannot stand in for the supported Policy API observation. +// A whole-customer claim cannot be derived from a top-OU value with unread child overrides. +fn workspace_admin_inspect(goal: WorkspaceAdminObligation, observation: WorkspaceAdminObservation, + admitted_admins: List, now_epoch: Int) -> GoalInspection { + let request = workspace_admin_read_surface(control: workspace_admin_control(subject: goal.subject)) + match observation { + WorkspaceAdminNotObserved { reason } => GoalObservationRefused { subject: goal.subject, goal: goal, request: request, cause: concat("not observed: ", reason as String) } + WorkspaceAdminReadRefused { reason } => GoalObservationRefused { subject: goal.subject, goal: goal, request: request, cause: concat("read refused: ", reason as String) } + WorkspaceAdminRead { receipt } => GoalInspected { subject: goal.subject, goal: goal, request: request, observed: receipt, + assessment: workspace_admin_assess(goal: goal, receipt: receipt, admitted_admins: admitted_admins, now_epoch: now_epoch) } + } +} + +fn workspace_admin_assess(goal: WorkspaceAdminObligation, receipt: WorkspaceAdminReadback, + admitted_admins: List, now_epoch: Int) -> GoalAssessment { + if receipt.subject != goal.subject { GoalAssessmentRefused { cause: "readback belongs to another customer/project/client/OU" } } + else if workspace_admin_control(subject: goal.subject) != workspace_admin_value_control(value: goal.desired) + || workspace_admin_control(subject: receipt.subject) != workspace_admin_value_control(value: receipt.observed) { + GoalAssessmentRefused { cause: "setting value belongs to another control" } + } else if receipt.observed_at_epoch > now_epoch || receipt.valid_until_epoch <= now_epoch + || receipt.valid_until_epoch <= receipt.observed_at_epoch { GoalAssessmentRefused { cause: "readback is future-dated, expired, or has no bounded lifetime" } } + else { match receipt.coverage { + EffectiveWithUnresolvedOverrides { reason } => GoalAssessmentRefused { cause: reason as String } + EffectiveAtDeclaredScope => { + let admitted = match receipt.source { + AdminConsoleReadback { administrator, evidence } => evidence as String != "" && any(admitted_admins, p => principal_ref_eq(a: p, b: administrator)) + && match workspace_admin_read_surface(control: workspace_admin_control(subject: goal.subject)) { ConsoleReadbackRequired { documentation: _ } => true _ => false } + WorkspacePolicyApiReadback { policy_resource, setting_type: observed_setting, evidence } => starts_with(s: policy_resource as String, prefix: "policies/") && evidence as String != "" + && match workspace_admin_read_surface(control: workspace_admin_control(subject: goal.subject)) { PolicyApiReadSupported { setting_type, documentation: _ } => setting_type == observed_setting _ => false } + } + if !admitted { GoalAssessmentRefused { cause: "readback source is not admitted for this setting" } } + else { assess_by_deviations(evidence: WorkspaceAdminEvidence { desired_revision: goal.desired_revision, receipt: receipt }, + deviations: if workspace_admin_values_equal(a: receipt.observed, b: goal.desired) { [] } else { [ WorkspaceAdminDifference { expected: goal.desired, observed: receipt.observed } ] }) } + } + } } +} + +// URI and scope lists are sets in the external policy, not a presentation-order contract. +fn workspace_admin_values_equal(a: WorkspaceAdminValue, b: WorkspaceAdminValue) -> Bool { + match a { + RegisteredWebClient { redirect_uris: left } => match b { + RegisteredWebClient { redirect_uris: right } => all(left, x => any(right, y => x == y)) && all(right, x => any(left, y => x == y)) + _ => false + } + ClientAccess { access: AppSpecificGoogleData { scopes: left } } => match b { + ClientAccess { access: AppSpecificGoogleData { scopes: right } } => all(left, x => any(right, y => x == y)) && all(right, x => any(left, y => x == y)) + _ => false + } + _ => a == b + } +} diff --git a/dag/gunbc/roadmap/roadmap_alignment.dag b/dag/gunbc/roadmap/roadmap_alignment.dag index 55890d07aa1..337af735d90 100644 --- a/dag/gunbc/roadmap/roadmap_alignment.dag +++ b/dag/gunbc/roadmap/roadmap_alignment.dag @@ -22,9 +22,10 @@ import extdeps.languages.json.parse { json_object_unique_member, JsonMemberFound, } -import gunbc.roadmap_model { RoadmapEdge, RoadmapNode, RoadmapNodeId } +import gunbc.roadmap_nesting { RoadmapMembership, declared_roadmap_memberships } +import gunbc.roadmap_model { RoadmapNode, RoadmapNodeId } import gunbc.roadmap_status { TicketLine } -import gunbc.roadmap_authority { declared_roadmap_edges, declared_roadmap_nodes } +import gunbc.roadmap_authority { declared_roadmap_nodes } import gunbc.roadmap.roadmap_project { ProjectMembership, MembershipStandalone, @@ -38,29 +39,8 @@ import gunbc.roadmap.roadmap_attempt_continuation { } import std.content_hash { content_hash_atom } -// THE ALIGNMENT LADDER, MODELED (docs/plans/harness-work-lifecycle.md, "Alignment ladder", owner -// direction 2026-09-21). Alignment is a recurring workflow responsibility at every level of one -// DERIVED chain: the task's contract (L2), the motivating project chain above it (L1, the arcs whose -// completion the task serves), and the root principles (L0, gunbc.design_document, pointed at, never -// inlined). Every level is accounted for because every level is DERIVED from the authority's own -// dependency edges -- change a row and the chain's rendering follows; there is no second list to -// drift. A node nothing depends on chains directly from its task level to the root principles: the -// honest projectless shape, not an error. -// -// THE MEMBERSHIP WALK IS THE ONLY PRODUCER, and it is not re-implemented here. The dependent rule -- -// edge(child: C, parent: P) means C depends on P, so P's motivating project is its DEPENDENT, the -// arc-that-depends-on-it, nearest first -- is gunbc.roadmap.roadmap_project -// node_project_membership's single authority (owner ruling 2026-09-20: membership is derived from -// the dependency edges, never rostered). This module CONSUMES that walk recursively: each step asks -// who owns the current node, and a node two arcs depend on is MembershipAmbiguous there -- a TYPED -// outcome, never a silent first(), because choosing would invent which level's intent the work -// serves. The roadmap graph is acyclic by authority, and the recursion is still fuel-bounded: one -// fold step per declared edge, so the walk always terminates under a bound the fold never trusts. -// -// CONTRIBUTION PRESERVATION: each link carries the node's declared purpose text (headline and -// boundary brief) projected from its authority row -- the derived statement of what that level -// contributes to the level above it. Alignment checks read against the carried contribution, never a -// fresh interpretation invented at check time. +// The recursive alignment ladder follows authored nesting, never reverse blocking. +// Each level carries its declared purpose and boundary. Ambiguous ownership refuses. type AlignmentChainLink { node_id: String purpose: String @@ -82,6 +62,7 @@ type AlignmentChain dependents: List, } | AlignmentChainRowUndeclared { node_id: String } + | AlignmentChainCyclic { node_id: String } fn alignment_root_pointer() -> DeclarationRef { decl_ref(module_path: "gunbc.design_document", decl_name: "design_document") @@ -143,14 +124,14 @@ fn alignment_walk_halt(acc: AlignmentWalk, ambiguous_dependents: List, u fn alignment_walk_step( nodes: List, - edges: List, + memberships: List, acc: AlignmentWalk, - fuel_edge: RoadmapEdge, + fuel_edge: RoadmapMembership, ) -> AlignmentWalk { if acc.halted { acc } else { - match node_project_membership(node_id: acc.current, edges: edges) { + match node_project_membership(node_id: acc.current, memberships: memberships) { MembershipStandalone => alignment_walk_halt(acc: acc, ambiguous_dependents: [], undeclared_halt: "") MembershipAmbiguous { arcs } => alignment_walk_halt(acc: acc, ambiguous_dependents: arcs, undeclared_halt: "") MembershipMember { arc } => @@ -175,12 +156,12 @@ fn alignment_walk_step( // edge's worth of budget, and a chain longer than the edge population is impossible by acyclicity -- // the fuel is the honest bound, not a guessed constant. An ambiguous step refuses with the prefix // already walked: the fork does not erase the derivation that reached it. -fn alignment_chain(node_id: String, edges: List, nodes: List) -> AlignmentChain { +fn alignment_chain(node_id: String, memberships: List, nodes: List) -> AlignmentChain { match alignment_link_project(node_id: node_id, nodes: nodes) { LinkRowUndeclared { node_id: missing } => AlignmentChainRowUndeclared { node_id: missing } LinkProjected { link: task } => { let walked = fold( - edges, + memberships, init: AlignmentWalk { current: node_id, projects: [], @@ -188,7 +169,7 @@ fn alignment_chain(node_id: String, edges: List, nodes: List alignment_walk_step(nodes: nodes, edges: edges, acc: acc, fuel_edge: fuel_edge), + f: (acc, fuel_edge) => alignment_walk_step(nodes: nodes, memberships: memberships, acc: acc, fuel_edge: fuel_edge), ) if count(walked.ambiguous_dependents) > 0 { AlignmentChainAmbiguous { @@ -196,6 +177,8 @@ fn alignment_chain(node_id: String, edges: List, nodes: List p.node_id == node_id || count(filter(walked.projects, q => q.node_id == p.node_id)) > 1) { + AlignmentChainCyclic { node_id: node_id } } else if walked.undeclared_halt != "" { AlignmentChainRowUndeclared { node_id: walked.undeclared_halt } } else { @@ -209,13 +192,13 @@ fn alignment_chain(node_id: String, edges: List, nodes: List AlignmentChain { alignment_chain( node_id: node_id, - edges: declared_roadmap_edges(), + memberships: declared_roadmap_memberships(), nodes: declared_roadmap_nodes(), ) } @@ -473,6 +456,7 @@ fn alignment_levels_due(chain: AlignmentResolvedChain, turns_elapsed: Int, state type AlignmentRefusal = AlignmentChainForked { at: String, dependents: List, walked: List } | AlignmentRowUndeclared { node_id: String } + | AlignmentNestingCyclic { node_id: String } fn alignment_refusal_reason(cause: AlignmentRefusal) -> String { match cause { @@ -482,6 +466,7 @@ fn alignment_refusal_reason(cause: AlignmentRefusal) -> String { " more than one dependent arc (", join(dependents, ", "), "); the authority must say which arc owns the delivery before the alignment chain can be read", ], "") + AlignmentNestingCyclic { node_id } => concat("Cyclic project nesting at ", node_id) AlignmentRowUndeclared { node_id } => join([ "the authority declares no ticket row for node ", node_id, @@ -591,6 +576,7 @@ fn alignment_resolve_chain(chain: AlignmentChain, turns_elapsed: Int, state: Ali } AlignmentChainAmbiguous { walked, at, dependents } => AlignmentUnresolved { cause: AlignmentChainForked { at: at, dependents: dependents, walked: walked } } + AlignmentChainCyclic { node_id } => AlignmentUnresolved { cause: AlignmentNestingCyclic { node_id: node_id } } AlignmentChainRowUndeclared { node_id } => AlignmentUnresolved { cause: AlignmentRowUndeclared { node_id: node_id } } } diff --git a/dag/gunbc/roadmap/roadmap_attempt_continuation.dag b/dag/gunbc/roadmap/roadmap_attempt_continuation.dag index f75ee02af14..d67b2a95f6e 100644 --- a/dag/gunbc/roadmap/roadmap_attempt_continuation.dag +++ b/dag/gunbc/roadmap/roadmap_attempt_continuation.dag @@ -1,5 +1,7 @@ module gunbc.roadmap.roadmap_attempt_continuation +import gunbc.roadmap_nesting { RoadmapMembership } + import std.types { String, Bool, Int, List, NonEmptyStr } import gunbc.roadmap_model { RoadmapEdge } import gunbc.roadmap.roadmap_submission { @@ -49,7 +51,7 @@ type AttemptOrigin // starting over from base. type PriorAttemptFacts { node_id: String - edges: List + memberships: List attempt_key: String branch: String worker_terminal: Bool @@ -335,7 +337,7 @@ fn continuation_at_cap(prior: PriorAttemptFacts, turn: Int, stall: String) -> Co // decomposition edges; a node whose membership is ambiguous has no supervisor to derive, and the // decision refuses with the ambiguity named rather than guessing one. fn continuation_escalate(prior: PriorAttemptFacts, turn: Int, stall: String) -> ContinuationDecision { - match supervisor_target_for(node_id: prior.node_id, edges: prior.edges) { + match supervisor_target_for(node_id: prior.node_id, memberships: prior.memberships) { Absent => ContinuationRefused { reason: join([ diff --git a/dag/gunbc/roadmap/roadmap_belt_actuate.dag b/dag/gunbc/roadmap/roadmap_belt_actuate.dag index e88347f6d0b..4f892702eef 100644 --- a/dag/gunbc/roadmap/roadmap_belt_actuate.dag +++ b/dag/gunbc/roadmap/roadmap_belt_actuate.dag @@ -1,5 +1,7 @@ module gunbc.roadmap_belt_actuate +import gunbc.roadmap_nesting { declared_roadmap_memberships } + import std.algebra { trim } import gunbc.output_policy { OutcomeIsData } @@ -2036,7 +2038,7 @@ fn belt_continuation_decision_for_progress(instance: HostDashboardInstance, node continuation_decision( prior: Present { value: PriorAttemptFacts { node_id: node.node as String, - edges: declared_roadmap_edges(), + memberships: declared_roadmap_memberships(), attempt_key: progress.attempt_key, branch: progress.branch, worker_terminal: belt_provider_is_terminal(provider: progress.provider), @@ -5992,10 +5994,10 @@ fn belt_scm_capture_candidate_tree( attempt_key: String, message: String, ) -> BeltScmCapture { - match project_key_for_node(node_id: node_id as String, edges: declared_roadmap_edges()) { + match project_key_for_node(node_id: node_id as String, memberships: declared_roadmap_memberships()) { Absent => ScmCapturePoisoned { - detail: "two or more arcs depend on this node, so its project cannot be derived from the dependency edges: the authority must name one owning arc before a candidate can be captured", + detail: "multiple authored parents name this node; project ownership is ambiguous: the authority must name one owning arc before a candidate can be captured", } Present { value: project_key } => let projects_dir = belt_scm_projects_dir(instance: instance) @@ -9994,6 +9996,10 @@ fn belt_integrate_candidate_core_for_instance( node_id: String, attempt_key: String, ) -> BeltIntegrationCoreStep { + if project_key_for_node(node_id: node_id, memberships: declared_roadmap_memberships()) == none { + IntegrationCoreUnreadable { detail: "project nesting is ambiguous or cyclic" } + } else { + let worktree = dispatch_worktree_path_for_instance( instance: instance, node_id: node_id as gunbc.roadmap_model.RoadmapNodeId, @@ -10044,10 +10050,10 @@ fn belt_integrate_candidate_core_for_instance( VerificationUnreadable { reason } => IntegrationCoreUnreadable { detail: reason } VerificationPassedConfirmed => { let edges = declared_roadmap_edges() - match node_project_membership(node_id: node_id, edges: edges) { + match node_project_membership(node_id: node_id, memberships: declared_roadmap_memberships()) { MembershipAmbiguous { arcs: _ } => IntegrationCoreUnreadable { - detail: "two or more arcs depend on this node, so its project cannot be derived from the dependency edges", + detail: "multiple authored parents name this node; project ownership is ambiguous", } membership => { let project_key = match membership { @@ -10077,6 +10083,7 @@ fn belt_integrate_candidate_core_for_instance( } } } + } } fn belt_integrate_candidate_for_instance( @@ -10090,9 +10097,9 @@ fn belt_integrate_candidate_for_instance( IntegrationCoreBlocked { detail } => IntegrationStepBlocked { detail: detail } IntegrationCoreIntegrated { project_key } => { let edges = declared_roadmap_edges() - let members = match node_project_membership(node_id: node_id, edges: edges) { + let members = match node_project_membership(node_id: node_id, memberships: declared_roadmap_memberships()) { MembershipStandalone => [node_id] - MembershipMember { arc } => project_member_ids(arc: arc, edges: edges) + MembershipMember { arc } => project_member_ids(arc: arc, memberships: declared_roadmap_memberships()) MembershipAmbiguous { arcs: _ } => [node_id] } match belt_integration_census(instance: instance, member_ids: members) { @@ -10209,7 +10216,7 @@ fn belt_publish_attempt_for_instance( ], ""), } } else { - match node_project_membership(node_id: node_id, edges: declared_roadmap_edges()) { + match node_project_membership(node_id: node_id, memberships: declared_roadmap_memberships()) { MembershipMember { arc } => belt_publish_project_for_instance( instance: instance, diff --git a/dag/gunbc/roadmap/roadmap_event_carrier.dag b/dag/gunbc/roadmap/roadmap_event_carrier.dag index 629fe6a7741..36c269c5115 100644 --- a/dag/gunbc/roadmap/roadmap_event_carrier.dag +++ b/dag/gunbc/roadmap/roadmap_event_carrier.dag @@ -3,6 +3,11 @@ module gunbc.roadmap.roadmap_event_carrier import std.types { String, Int, Bool, List, NonEmptyStr, FilePath, GitRef } import extdeps.filesystem.filesystem_io { Filesystem } import std.algebra { trim } +import std.content_hash { content_hash_of_value } +import std.durable_compare_and_set { CasGeneration, CasReadableSlot, CasReadableAbsent, CasReadablePresent, CasSlotVersion, ExpectSlotAbsent, ExpectSlotGeneration, cas_expectation_admits } +import gunbc.roadmap.roadmap_event_log { roadmap_event_chain } +import extdeps.shell +import gunbc.roadmap.roadmap_event_log { roadmap_event_heads, roadmap_node_standing_key, roadmap_issue_state } import gunbc.output_policy { OutcomeIsData } import extdeps.gunbc import extdeps.git { @@ -20,23 +25,12 @@ import gunbc.roadmap.roadmap_event_log { NodeStandingRow, RoadmapStandings, StandingsObserved, StandingsUnobserved, roadmap_node_standing, } -// THE CARRIER IS TWO OPERATIONS AND NOTHING ELSE: append one event; read one node's events. Everything -// about WHERE the events live is a realization behind those two, and this module holds the first one -// -- a git branch, `roadmap-events`, checked out in a worktree of the instance's repository, one file -// per event at events//.json, pushed to origin on every append and fetched before every -// read. It is the realization because git is what every host already has, it is auditable, and a -// content-addressed file per event makes concurrent writers converge at the file level (two hosts -// writing the same event write the same path with the same bytes). It is NOT the design: the model -// (gunbc.roadmap.roadmap_event_log) knows nothing about branches, and a store or a stream realizes -// the same two operations later without touching the fold (operator, 2026-09-05: this becomes a -// widely distributed stateless service; the git ref must be a handler, not the shape). -// -// WHAT THIS REALIZATION REFUSES RATHER THAN HIDES. A push the remote rejects (another writer got -// there first) is EventAppendRefused with the remote's words; the caller re-reads and appends again, -// and because the event file is content-addressed the retry produces the same file, never a second -// event. A read whose fetch fails is EventsReadRefused, never a stale set presented as current: the -// last-fetched state is not "the events", it is "the events as of a fetch that may be old", and the -// carrier has no way to say how old, so it says nothing. +// The issue command boundary is carrier-independent. This Git realization publishes the issue +// revision and operation receipt together with an exact remote-ref compare-and-swap. Each read +// or append owns a private snapshot; fetching cannot reset another request's index or precondition. +// A failed read remains a refusal. Publication conflicts never rebase a stale issue command. +// Operation retries bind issue, principal, payload and original precondition, and recover the +// committed event before checking the current revision. Timestamps are result data, not identity. data roadmap_events_branch: NonEmptyStr = "roadmap-events" as NonEmptyStr data roadmap_events_remote: String = "origin" data roadmap_events_dir_segment: String = "events" @@ -136,7 +130,7 @@ fn roadmap_event_carrier_ensure(layout: RoadmapEventCarrierLayout) -> CarrierEns match carrier_exec(layout: layout, workdir: layout.worktree as String, args: shape_git_checkout_orphan_args(branch: roadmap_events_branch)) { CarrierExecFailed { exit_code, stderr } => CarrierEnsureRefused { step: "checkout-orphan", reason: join(["exit ", to_string(value: exit_code), ": ", stderr], "") } CarrierExecOk { stdout: _ } => - match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["rm", "-rfq", "--cached", "."]) { + match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["rm", "-rfq", "--cached", "--ignore-unmatch", "."]) { CarrierExecFailed { exit_code, stderr } => CarrierEnsureRefused { step: "orphan-unstage", reason: join(["exit ", to_string(value: exit_code), ": ", stderr], "") } CarrierExecOk { stdout: _ } => match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["clean", "-fdq"]) { @@ -173,45 +167,141 @@ fn roadmap_event_carrier_sync(layout: RoadmapEventCarrierLayout) -> CarrierEnsur } } -fn roadmap_event_append(layout: RoadmapEventCarrierLayout, event: RoadmapEvent) -> RoadmapEventAppend { - let env = roadmap_event_envelope(e: event) +// Every operation owns its worktree. A concurrent read cannot reset a writer's index or FETCH_HEAD. +// The remote publication is conditional on the EXACT fetched commit, not merely fast-forward. +// The issue-parent decision and operation receipt are made inside that same immutable snapshot. +type CarrierSnapshot = CarrierSnapshotReady { layout: RoadmapEventCarrierLayout } | CarrierSnapshotRefused { reason: String } + +fn roadmap_event_private_snapshot(layout: RoadmapEventCarrierLayout) -> CarrierSnapshot { match roadmap_event_carrier_ensure(layout: layout) { - CarrierEnsureRefused { step, reason } => EventAppendRefused { id: env.id, step: join(["ensure/", step], ""), reason: reason } - CarrierReady => - match roadmap_event_carrier_sync(layout: layout) { - CarrierEnsureRefused { step, reason } => EventAppendRefused { id: env.id, step: join(["sync/", step], ""), reason: reason } - CarrierReady => { - let rel = roadmap_event_file_relative(node: event.node, id: env.id) - let path = join([layout.worktree as String, "/", rel], "") - let existing = Filesystem.Read(path: path) - if existing.success { - EventAlreadyPresent { id: env.id } - } else { - match carrier_exec_program(program: layout.directory_program, workdir: layout.worktree as String, args: ["-d", roadmap_event_node_dir(layout: layout, node: event.node)]) { - CarrierExecFailed { exit_code, stderr } => EventAppendRefused { id: env.id, step: "mkdir", reason: join(["exit ", to_string(value: exit_code), ": ", stderr], "") } - CarrierExecOk { stdout: _ } => { - let w = Filesystem.Write(path: path, content: roadmap_event_wire_text(env: env)) - if !w.success { - EventAppendRefused { id: env.id, step: "write", reason: w.error } - } else { - match carrier_exec(layout: layout, workdir: layout.worktree as String, args: shape_git_add_path_args(path: rel)) { - CarrierExecFailed { exit_code, stderr } => EventAppendRefused { id: env.id, step: "add", reason: join(["exit ", to_string(value: exit_code), ": ", stderr], "") } - CarrierExecOk { stdout: _ } => - match carrier_exec(layout: layout, workdir: layout.worktree as String, args: shape_git_commit_message_args(message: roadmap_event_commit_message(env: env))) { - CarrierExecFailed { exit_code, stderr } => EventAppendRefused { id: env.id, step: "commit", reason: join(["exit ", to_string(value: exit_code), ": ", stderr], "") } - CarrierExecOk { stdout: _ } => - match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["push", "-q", roadmap_events_remote, roadmap_event_push_refspec()]) { - CarrierExecFailed { exit_code, stderr } => EventAppendRefused { id: env.id, step: "push", reason: join(["the remote refused the push (exit ", to_string(value: exit_code), "); another writer may have appended first — re-read and append again: ", stderr], "") } - CarrierExecOk { stdout: _ } => EventAppended { id: env.id } - } - } - } - } - } + CarrierEnsureRefused { step, reason } => CarrierSnapshotRefused { reason: concat(step, concat(": ", reason)) } + CarrierReady => { + let temporary = shell.Mktemp.Dir() + if !temporary.success { CarrierSnapshotRefused { reason: "cannot create private event snapshot" } } + else { match carrier_exec(layout: layout, workdir: layout.repo_root as String, + args: ["worktree", "add", "--detach", "--no-checkout", temporary.path, "HEAD"]) { + CarrierExecFailed { exit_code: _, stderr } => CarrierSnapshotRefused { reason: stderr } + CarrierExecOk { stdout: _ } => CarrierSnapshotReady { layout: RoadmapEventCarrierLayout { + repo_root: layout.repo_root, worktree: temporary.path as FilePath, git_program: layout.git_program, directory_program: layout.directory_program } } + } } + } + } +} + +fn roadmap_event_snapshot_close(layout: RoadmapEventCarrierLayout) -> CarrierExec { + carrier_exec(layout: layout, workdir: layout.repo_root as String, args: ["worktree", "remove", "--force", layout.worktree as String]) +} + +fn roadmap_event_append(layout: RoadmapEventCarrierLayout, event: RoadmapEvent, operation: String = "") -> RoadmapEventAppend { + if operation != "" && !roadmap_operation_identity_valid(operation: operation) { EventAppendRefused { id: roadmap_event_envelope(e: event).id, step: "operation-invalid", reason: "operation identity must be 1–128 URL-safe ASCII characters" } } else { + match roadmap_event_private_snapshot(layout: layout) { + CarrierSnapshotRefused { reason } => EventAppendRefused { id: roadmap_event_envelope(e: event).id, step: "snapshot", reason: reason } + CarrierSnapshotReady { layout: private } => { + let result = roadmap_event_append_private(layout: private, event: event, operation: operation) + let cleanup = roadmap_event_snapshot_close(layout: private) + result + } + } +} + +} + +fn roadmap_operation_identity_valid(operation: String) -> Bool { + string_length(s: operation) > 0 && string_length(s: operation) <= 128 + && all(operation |> chars, cp => (cp >= 48 && cp <= 57) || (cp >= 65 && cp <= 90) || (cp >= 97 && cp <= 122) || cp == 45 || cp == 95) +} + +fn roadmap_event_operation_path(event: RoadmapEvent, operation: String) -> String { + join(["operations/", event.node as String, "/", operation, ".json"], "") +} + +// recorded_at belongs to the committed result, not the retry payload. Parent remains bound to +// the original precondition; a key cannot be repurposed for a new command after a later event. +fn roadmap_event_same_command(a: RoadmapEvent, b: RoadmapEvent) -> Bool { + a.node == b.node && a.author == b.author && a.parent == b.parent && a.kind == b.kind +} + +fn roadmap_event_append_private(layout: RoadmapEventCarrierLayout, event: RoadmapEvent, operation: String) -> RoadmapEventAppend { + let env = roadmap_event_envelope(e: event) + match roadmap_event_carrier_sync(layout: layout) { + CarrierEnsureRefused { step, reason } => EventAppendRefused { id: env.id, step: step, reason: reason } + CarrierReady => match carrier_exec(layout: layout, workdir: layout.worktree as String, args: ["rev-parse", "HEAD"]) { + CarrierExecFailed { exit_code: _, stderr } => EventAppendRefused { id: env.id, step: "snapshot-revision", reason: stderr } + CarrierExecOk { stdout: revision } => { + let receipt_path = roadmap_event_operation_path(event: event, operation: operation) + let receipt = Filesystem.Read(path: concat(layout.worktree as String, concat("/", receipt_path))) + if operation != "" && receipt.success { + match roadmap_event_decode(text: receipt.content) { + RoadmapEventUndecodable { reason } => EventAppendRefused { id: env.id, step: "operation-read", reason: reason } + RoadmapEventDecoded { envelope: previous } => if roadmap_event_same_command(a: previous.event, b: event) { + let committed = Filesystem.Read(path: concat(layout.worktree as String, concat("/", roadmap_event_file_relative(node: previous.event.node, id: previous.id)))) + if committed.success && committed.content == receipt.content { EventAlreadyPresent { id: previous.id } } + else { EventAppendRefused { id: env.id, step: "operation-read", reason: "operation receipt has no matching committed event" } } + } else { EventAppendRefused { id: env.id, step: "operation-conflict", reason: "operation identity already belongs to another issue/principal/payload/precondition" } } + } + } else if operation != "" && receipt.error_kind != "not_found" { + EventAppendRefused { id: env.id, step: "operation-read", reason: receipt.error } + } else { match roadmap_events_read_synced(layout: layout, node: event.node) { + EventsReadRefused { node: _, step, reason } => EventAppendRefused { id: env.id, step: step, reason: reason } + EventsRead { node: _, envelopes } => if any(envelopes, existing => existing.id == env.id) { EventAlreadyPresent { id: env.id } } + else if !roadmap_event_parent_admitted(event: event, envelopes: envelopes) { + EventAppendRefused { id: env.id, step: "revision-conflict", reason: "expected issue revision does not match the committed issue head" } + } else { roadmap_event_publish_private(layout: layout, env: env, operation: operation, receipt_path: receipt_path, expected_commit: trim(revision)) } + } } + } + } + } +} + +fn roadmap_event_parent_admitted(event: RoadmapEvent, envelopes: List) -> Bool { + let standing = roadmap_node_standing_key(s: roadmap_node_standing(envs: envelopes)) + if standing == "forked" || standing == "history_incomplete" { false } + else if any(envelopes, e => e.event.node != event.node) { false } + else { + let readable = match roadmap_event_heads(envs: envelopes).first() { + Absent => CasReadableAbsent + Present { value: head } => CasReadablePresent { version: CasSlotVersion { + generation: count(envelopes), content: content_hash_of_value(value: head.id as NonEmptyStr), value: head.id } } + } + match event.parent { + Absent => cas_expectation_admits(expected: ExpectSlotAbsent, readable: readable) + Present { value: expected } => match filter(envelopes, e => e.id == expected).first() { + Absent => false + Present { value: previous } => cas_expectation_admits(expected: ExpectSlotGeneration { + generation: count(roadmap_event_chain(envs: envelopes, head: previous, budget: count(envelopes))) }, readable: readable) + } + } + } +} + +fn roadmap_event_publish_private(layout: RoadmapEventCarrierLayout, env: RoadmapEventEnvelope, + operation: String, receipt_path: String, expected_commit: String) -> RoadmapEventAppend { + let rel = roadmap_event_file_relative(node: env.event.node, id: env.id) + match carrier_exec_program(program: layout.directory_program, workdir: layout.worktree as String, + args: ["-d", roadmap_event_node_dir(layout: layout, node: env.event.node), concat(layout.worktree as String, concat("/operations/", env.event.node as String))]) { + CarrierExecFailed { exit_code: _, stderr } => EventAppendRefused { id: env.id, step: "mkdir", reason: stderr } + CarrierExecOk { stdout: _ } => { + let written = Filesystem.Write(path: concat(layout.worktree as String, concat("/", rel)), content: roadmap_event_wire_text(env: env)) + let recorded = if operation == "" { written.success } else { Filesystem.Write(path: concat(layout.worktree as String, concat("/", receipt_path)), content: roadmap_event_wire_text(env: env)).success } + if !written.success || !recorded { EventAppendRefused { id: env.id, step: "write", reason: "event or operation receipt write refused" } } + else { match carrier_exec(layout: layout, workdir: layout.worktree as String, args: concat(["add", "--", rel], if operation == "" { [] } else { [receipt_path] })) { + CarrierExecFailed { exit_code: _, stderr } => EventAppendRefused { id: env.id, step: "add", reason: stderr } + CarrierExecOk { stdout: _ } => match carrier_exec(layout: layout, workdir: layout.worktree as String, args: shape_git_commit_message_args(message: roadmap_event_commit_message(env: env))) { + CarrierExecFailed { exit_code: _, stderr } => EventAppendRefused { id: env.id, step: "commit", reason: stderr } + CarrierExecOk { stdout: _ } => match carrier_exec(layout: layout, workdir: layout.worktree as String, + args: ["push", "-q", concat("--force-with-lease=refs/heads/roadmap-events:", expected_commit), roadmap_events_remote, "HEAD:refs/heads/roadmap-events"]) { + CarrierExecFailed { exit_code: _, stderr } => match carrier_exec(layout: layout, workdir: layout.worktree as String, + args: shape_git_ls_remote_heads_args(remote: roadmap_events_remote, branch: roadmap_events_branch)) { + CarrierExecOk { stdout: current } => EventAppendRefused { id: env.id, + step: if trim(current) != "" && !starts_with(s: current, prefix: concat(expected_commit, "\t")) { "revision-conflict" } else { "publication-refused" }, + reason: concat("conditional publication refused; retry using the same operation identity: ", stderr) } + _ => EventAppendRefused { id: env.id, step: "publication-refused", reason: stderr } } + CarrierExecOk { stdout: _ } => EventAppended { id: env.id } } } - } + } } + } } } @@ -243,7 +333,7 @@ fn roadmap_events_decode_reads(node: RoadmapNodeId, reads: List) } } -fn roadmap_events_read(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) -> RoadmapEventsRead { +fn roadmap_events_read_private(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) -> RoadmapEventsRead { match roadmap_event_carrier_ensure(layout: layout) { CarrierEnsureRefused { step, reason } => EventsReadRefused { node: node, step: join(["ensure/", step], ""), reason: reason } CarrierReady => @@ -252,7 +342,7 @@ fn roadmap_events_read(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) - CarrierReady => { let dir = roadmap_event_node_dir(layout: layout, node: node) let listing = Filesystem.List(path: dir) - if !listing.success { + if !listing.success && listing.error_kind != "not_found" { EventsReadRefused { node: node, step: "list", reason: listing.error } } else if !listing.success { EventsRead { node: node, envelopes: [] } } else { roadmap_events_decode_all(node: node, dir: dir, names: filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "")) @@ -265,7 +355,7 @@ fn roadmap_events_read(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) - // EVERY NODE'S STANDING IN ONE SYNC: one fetch, then one fold per node directory. The belt's served // observation reads this on every tick so the board shows the log without a merge; a carrier that // cannot be reached is StandingsUnobserved with the step that failed, never an empty roster. -fn roadmap_standings_read(layout: RoadmapEventCarrierLayout) -> RoadmapStandings { +fn roadmap_standings_read_private(layout: RoadmapEventCarrierLayout) -> RoadmapStandings { match roadmap_event_carrier_ensure(layout: layout) { CarrierEnsureRefused { step, reason } => StandingsUnobserved { reason: join(["ensure/", step, ": ", reason], "") } CarrierReady => @@ -274,7 +364,7 @@ fn roadmap_standings_read(layout: RoadmapEventCarrierLayout) -> RoadmapStandings CarrierReady => { let root = join([layout.worktree as String, "/", roadmap_events_dir_segment], "") let listing = Filesystem.List(path: root) - if !listing.success { + if !listing.success && listing.error_kind != "not_found" { StandingsUnobserved { reason: listing.error } } else if !listing.success { StandingsObserved { rows: [] } } else { let nodes = filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "") @@ -282,7 +372,8 @@ fn roadmap_standings_read(layout: RoadmapEventCarrierLayout) -> RoadmapStandings match (filter(reads, r => match r { EventsReadRefused { node: _, step: _, reason: _ } => true EventsRead { node: _, envelopes: _ } => false })).first() { Present { value: EventsReadRefused { node, step, reason } } => StandingsUnobserved { reason: join(["node ", node as String, " ", step, ": ", reason], "") } _ => StandingsObserved { rows: fold(reads, init: [], f: (acc, r) => match r { - EventsRead { node, envelopes } => concat(acc, [NodeStandingRow { node_id: node as String, standing: roadmap_node_standing(envs: envelopes) }]) + EventsRead { node, envelopes } => { let state = roadmap_issue_state(envs: envelopes) + concat(acc, [NodeStandingRow { node_id: node as String, standing: state.standing, assignment: state.assignment }]) } EventsReadRefused { node: _, step: _, reason: _ } => acc }) } } @@ -296,7 +387,7 @@ fn roadmap_standings_read(layout: RoadmapEventCarrierLayout) -> RoadmapStandings fn roadmap_events_read_synced(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) -> RoadmapEventsRead { let dir = roadmap_event_node_dir(layout: layout, node: node) let listing = Filesystem.List(path: dir) - if !listing.success { + if !listing.success && listing.error_kind != "not_found" { EventsReadRefused { node: node, step: "list", reason: listing.error } } else if !listing.success { EventsRead { node: node, envelopes: [] } } else { roadmap_events_decode_all(node: node, dir: dir, names: filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "")) @@ -325,7 +416,7 @@ fn roadmap_all_events_of_reads(reads: List) -> RoadmapAllEven } } -fn roadmap_events_read_all(layout: RoadmapEventCarrierLayout) -> RoadmapAllEventsRead { +fn roadmap_events_read_all_private(layout: RoadmapEventCarrierLayout) -> RoadmapAllEventsRead { match roadmap_event_carrier_ensure(layout: layout) { CarrierEnsureRefused { step, reason } => AllEventsReadRefused { step: join(["ensure/", step], ""), reason: reason } CarrierReady => @@ -334,7 +425,7 @@ fn roadmap_events_read_all(layout: RoadmapEventCarrierLayout) -> RoadmapAllEvent CarrierReady => { let root = join([layout.worktree as String, "/", roadmap_events_dir_segment], "") let listing = Filesystem.List(path: root) - if !listing.success { + if !listing.success && listing.error_kind != "not_found" { AllEventsReadRefused { step: "list", reason: listing.error } } else if !listing.success { AllEventsRead { envelopes: [] } } else { let nodes = filter(split(s: listing.entries, delimiter: "\n"), n => trim(n) != "") @@ -344,3 +435,36 @@ fn roadmap_events_read_all(layout: RoadmapEventCarrierLayout) -> RoadmapAllEvent } } } + +fn roadmap_events_read(layout: RoadmapEventCarrierLayout, node: RoadmapNodeId) -> RoadmapEventsRead { + match roadmap_event_private_snapshot(layout: layout) { + CarrierSnapshotRefused { reason } => EventsReadRefused { node: node, step: "snapshot", reason: reason } + CarrierSnapshotReady { layout: private } => { + let result = roadmap_events_read_private(layout: private, node: node) + let cleanup = roadmap_event_snapshot_close(layout: private) + result + } + } +} + +fn roadmap_standings_read(layout: RoadmapEventCarrierLayout) -> RoadmapStandings { + match roadmap_event_private_snapshot(layout: layout) { + CarrierSnapshotRefused { reason } => StandingsUnobserved { reason: reason } + CarrierSnapshotReady { layout: private } => { + let result = roadmap_standings_read_private(layout: private) + let cleanup = roadmap_event_snapshot_close(layout: private) + result + } + } +} + +fn roadmap_events_read_all(layout: RoadmapEventCarrierLayout) -> RoadmapAllEventsRead { + match roadmap_event_private_snapshot(layout: layout) { + CarrierSnapshotRefused { reason } => AllEventsReadRefused { step: "snapshot", reason: reason } + CarrierSnapshotReady { layout: private } => { + let result = roadmap_events_read_all_private(layout: private) + let cleanup = roadmap_event_snapshot_close(layout: private) + result + } + } +} diff --git a/dag/gunbc/roadmap/roadmap_event_log.dag b/dag/gunbc/roadmap/roadmap_event_log.dag index b6df67dc591..6f52a16e4bf 100644 --- a/dag/gunbc/roadmap/roadmap_event_log.dag +++ b/dag/gunbc/roadmap/roadmap_event_log.dag @@ -486,30 +486,30 @@ fn roadmap_event_chain(envs: List, head: RoadmapEventEnvel // and rides the acc until a PublicationObserved spends it. type RoadmapStandingAcc { standing: RoadmapNodeStanding - return_to: EventPrincipal? + assignment: IssueAssignmentProjection } fn roadmap_standing_acc_empty() -> RoadmapStandingAcc { - RoadmapStandingAcc { standing: NodeOpen, return_to: none } + RoadmapStandingAcc { standing: NodeOpen, assignment: roadmap_assignment_empty() } } -// THE RETURN-TO-MERGE HANDOFF (owner mandate 2026-09-25): an observed publication moves the -// holder to the stored return_to and the standing reads READY FOR REVIEW — merge remains the -// human completion action; publication never closes the issue. A publication on a history with -// no recorded return_to changes no holder (the acc passes through, honestly). +// One transition authority produces progress and ownership together. Progress/blocked/handback +// authors are actors, not implicit assignees. Only explicit assignment transitions change holder. fn roadmap_standing_step_acc(acc: RoadmapStandingAcc, env: RoadmapEventEnvelope) -> RoadmapStandingAcc { let e = env.event match e.kind { - Claimed { assignee, return_to } => RoadmapStandingAcc { standing: NodeClaimed { by: assignee, since: e.recorded_at }, return_to: Present { value: return_to } } - Released => RoadmapStandingAcc { standing: NodeOpen, return_to: none } - Progress { note } => RoadmapStandingAcc { standing: NodeInProgress { by: e.author, last_note: note }, return_to: acc.return_to } - Blocked { reason } => RoadmapStandingAcc { standing: NodeBlocked { by: e.author, reason: reason }, return_to: acc.return_to } - HandedBack { reason } => RoadmapStandingAcc { standing: NodeHandedBack { by: e.author, reason: reason }, return_to: acc.return_to } - PublicationObserved { pr, head } => - match acc.return_to { - Present { value: return_to } => RoadmapStandingAcc { standing: NodeReadyForReview { by: return_to, pr: pr, head: head }, return_to: acc.return_to } - Absent => acc - } + Claimed { assignee, return_to } => RoadmapStandingAcc { + standing: NodeClaimed { by: assignee, since: e.recorded_at }, + assignment: IssueAssignmentProjection { holder: Present { value: assignee }, return_to: Present { value: return_to } } } + Released => roadmap_standing_acc_empty() + Progress { note } => RoadmapStandingAcc { standing: NodeInProgress { by: e.author, last_note: note }, assignment: acc.assignment } + Blocked { reason } => RoadmapStandingAcc { standing: NodeBlocked { by: e.author, reason: reason }, assignment: acc.assignment } + HandedBack { reason } => RoadmapStandingAcc { standing: NodeHandedBack { by: e.author, reason: reason }, assignment: acc.assignment } + PublicationObserved { pr, head } => match acc.assignment.return_to { + Present { value: return_to } => RoadmapStandingAcc { standing: NodeReadyForReview { by: return_to, pr: pr, head: head }, + assignment: IssueAssignmentProjection { holder: Present { value: return_to }, return_to: acc.assignment.return_to } } + Absent => acc + } ReceiptReferenced { obligation: _, address: _, verdict: _ } => acc CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => acc CommentEdited { comment: _, body: _ } => acc @@ -517,31 +517,31 @@ fn roadmap_standing_step_acc(acc: RoadmapStandingAcc, env: RoadmapEventEnvelope) } } -fn roadmap_node_standing(envs: List) -> RoadmapNodeStanding { +fn roadmap_issue_state(envs: List) -> RoadmapStandingAcc { let missing = roadmap_event_missing_parents(envs: envs) if count(missing) > 0 { - NodeHistoryIncomplete { missing_parents: missing } + RoadmapStandingAcc { standing: NodeHistoryIncomplete { missing_parents: missing }, assignment: roadmap_assignment_empty() } } else { let heads = roadmap_event_heads(envs: envs) - if count(envs) == 0 { - NodeOpen - } else if count(heads) != 1 { - NodeForked { heads: map(heads, h => h.id) } - } else { - match heads.first() { - Absent => NodeOpen - Present { value: head } => - (fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: roadmap_standing_acc_empty(), f: (acc, env) => roadmap_standing_step_acc(acc: acc, env: env))).standing - } - } + if count(envs) == 0 { roadmap_standing_acc_empty() } + else if count(heads) != 1 { RoadmapStandingAcc { standing: NodeForked { heads: map(heads, h => h.id) }, assignment: roadmap_assignment_empty() } } + else { match heads.first() { + Absent => roadmap_standing_acc_empty() + Present { value: head } => fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: roadmap_standing_acc_empty(), f: (acc, env) => roadmap_standing_step_acc(acc: acc, env: env)) + } } } } +fn roadmap_node_standing(envs: List) -> RoadmapNodeStanding { + roadmap_issue_state(envs: envs).standing +} + // ONE ROW PER NODE THAT HAS EVENTS, and the observation that wraps the roster: a reader that could not // reach the carrier says so once, never renders every node as untouched. type NodeStandingRow { node_id: String standing: RoadmapNodeStanding + assignment: IssueAssignmentProjection } type RoadmapStandings @@ -661,39 +661,16 @@ fn roadmap_assignment_empty() -> IssueAssignmentProjection { IssueAssignmentProjection { holder: none, return_to: none } } -fn roadmap_assignment_step(acc: IssueAssignmentProjection, env: RoadmapEventEnvelope) -> IssueAssignmentProjection { - match env.event.kind { - Claimed { assignee, return_to } => IssueAssignmentProjection { holder: Present { value: assignee }, return_to: Present { value: return_to } } - Released => roadmap_assignment_empty() - Progress { note: _ } => acc - Blocked { reason: _ } => acc - HandedBack { reason: _ } => acc - ReceiptReferenced { obligation: _, address: _, verdict: _ } => acc - CommentCreated { comment: _, body: _, visibility: _, parent_comment: _ } => acc - CommentEdited { comment: _, body: _ } => acc - CommentDeleted { comment: _ } => acc - PublicationObserved { pr: _, head: _ } => - match acc.return_to { - Present { value: return_to } => IssueAssignmentProjection { holder: Present { value: return_to }, return_to: acc.return_to } - Absent => acc - } - } +fn roadmap_issue_assignment(envs: List) -> IssueAssignmentProjection { + roadmap_issue_state(envs: envs).assignment } -fn roadmap_issue_assignment(envs: List) -> IssueAssignmentProjection { - let missing = roadmap_event_missing_parents(envs: envs) - if count(missing) > 0 { - roadmap_assignment_empty() - } else { - let heads = roadmap_event_heads(envs: envs) - if count(envs) == 0 || count(heads) != 1 { - roadmap_assignment_empty() - } else { - match heads.first() { - Absent => roadmap_assignment_empty() - Present { value: head } => - fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: roadmap_assignment_empty(), f: (acc, env) => roadmap_assignment_step(acc: acc, env: env)) - } +fn assignment_for_node(standings: RoadmapStandings, node_id: String) -> IssueAssignmentProjection? { + match standings { + StandingsUnobserved { reason: _ } => none + StandingsObserved { rows } => match filter(rows, r => r.node_id == node_id).first() { + Present { value: row } => Present { value: row.assignment } + Absent => none } } } @@ -834,3 +811,15 @@ fn issue_comment_anchor(ordinal: Int) -> String { fn roadmap_publication_handoff_event(node: RoadmapNodeId, pr: NonEmptyStr, head: NonEmptyStr, author: EventPrincipal, parent: RoadmapEventId?, recorded_at: String) -> RoadmapEvent { RoadmapEvent { node: node, author: author, parent: parent, kind: PublicationObserved { pr: pr, head: head }, recorded_at: recorded_at } } + +// A single issue's audit ordering is causal. Cross-issue recents retain their separate clock +// ordering; they cannot define a shared causal chain. Invalid histories retain their raw events +// for diagnosis, and the standing/route boundary refuses to advertise a current issue snapshot. +fn roadmap_issue_history_newest_first(envs: List) -> List { + let heads = roadmap_event_heads(envs: envs) + if count(heads) != 1 || count(roadmap_event_missing_parents(envs: envs)) > 0 { envs } + else { match heads.first() { + Absent => envs + Present { value: head } => fold(roadmap_event_chain(envs: envs, head: head, budget: count(envs)), init: [], f: (acc, event) => concat([event], acc)) + } } +} diff --git a/dag/gunbc/roadmap/roadmap_fleet_components.dag b/dag/gunbc/roadmap/roadmap_fleet_components.dag new file mode 100644 index 00000000000..e1d8bff8278 --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_fleet_components.dag @@ -0,0 +1,44 @@ +module gunbc.roadmap_fleet_components + +import std.types { String, List, Int } +import product.placement_supply { HostIdentity } +import extdeps.google.issue_tracker { Component } +import gunbc.fleet_intent_network { + operator_host_srv1, operator_host_srv2, operator_host_srv3, operator_host_srv4, + operator_host_srv5, operator_host_srv6, operator_host_srv7, operator_host_srv8, + operator_host_srv9, operator_host_srv10, operator_host_srv11, operator_host_srv12, +} +import gunbc.spark.fabric_switch_observed { fabric_group_of_host, fabric_group_wire } + +// Tracker component identifiers are existing external identities, retained across this move. +// Host names come from fleet identity; serving placement comes from the fleet's observed group +// relation, never a second group/host table under Google's interface model. +type FleetComponentBinding { id: Int, host: HostIdentity } +data fleet_component_bindings: List = [ + FleetComponentBinding { id: 1, host: operator_host_srv1 }, + FleetComponentBinding { id: 2, host: operator_host_srv2 }, + FleetComponentBinding { id: 3, host: operator_host_srv3 }, + FleetComponentBinding { id: 4, host: operator_host_srv4 }, + FleetComponentBinding { id: 5, host: operator_host_srv5 }, + FleetComponentBinding { id: 6, host: operator_host_srv6 }, + FleetComponentBinding { id: 7, host: operator_host_srv7 }, + FleetComponentBinding { id: 8, host: operator_host_srv8 }, + FleetComponentBinding { id: 9, host: operator_host_srv9 }, + FleetComponentBinding { id: 10, host: operator_host_srv10 }, + FleetComponentBinding { id: 11, host: operator_host_srv11 }, + FleetComponentBinding { id: 12, host: operator_host_srv12 }, +] + +data fleet_component_hosts: List = map(fleet_component_bindings, b => Component { id: b.id, path: [b.host as String] }) + +fn fleet_component_serving_arms() -> List { + flat_map(fleet_component_bindings, b => match fabric_group_of_host(host: b.host) { + Absent => [] + Present { value: group } => [Component { id: 100 + b.id, path: [b.host as String, "serving", fabric_group_wire(g: group) as String] }] + }) +} + +fn fleet_components() -> List { concat(fleet_component_hosts, fleet_component_serving_arms()) } +fn spark_host_component_paths() -> List { + flat_map(fleet_component_bindings, b => match fabric_group_of_host(host: b.host) { Absent => [] Present { value: _ } => [b.host as String] }) +} diff --git a/dag/gunbc/roadmap/roadmap_inline_assignment.dag b/dag/gunbc/roadmap/roadmap_inline_assignment.dag index 1045e671973..eb4b0df226e 100644 --- a/dag/gunbc/roadmap/roadmap_inline_assignment.dag +++ b/dag/gunbc/roadmap/roadmap_inline_assignment.dag @@ -39,8 +39,9 @@ fn inline_assignment_option(session: AuthenticatedSession, p: EventPrincipal) -> ]) } -fn inline_assignment_context_body(session: AuthenticatedSession, events: List, recents: List) -> String { +fn inline_assignment_context_body(session: AuthenticatedSession, events: List, recents: List, operation: String = "") -> String { serialize_json(v: json_object(members: [ + json_kv(key: "key", value: json_string(s: operation)), json_kv(key: "csrf", value: json_string(s: session.csrf.token as String)), json_kv(key: "expected_revision", value: json_string(s: issue_chain_head_id(envs: events))), json_kv(key: "options", value: json_array(elements: map(inline_assignment_choices(session: session, recents: recents), p => inline_assignment_option(session: session, p: p)))), diff --git a/dag/gunbc/roadmap/roadmap_inline_assignment_client.dag b/dag/gunbc/roadmap/roadmap_inline_assignment_client.dag index 463988e548a..c23efcd9ac4 100644 --- a/dag/gunbc/roadmap/roadmap_inline_assignment_client.dag +++ b/dag/gunbc/roadmap/roadmap_inline_assignment_client.dag @@ -41,8 +41,8 @@ fn inline_assignment_bind_statements() -> List { ts_const(name: "assignmentPost", value: ts_arrow(params: ["path", "context", "assignee", "container"], body: ts_block(stmts: [ ts_const(name: "result", value: ts_call(callee: ts_ident(name: "assignmentResult"), args: [ts_ident(name: "container")])), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "result"), property: "textContent"), op: "=", right: ts_lit_str(value: "Recording assignment…"))), - ts_const(name: "body", value: ts_call(callee: ts_member(object: ts_array(elements: [ts_binop(left: ts_lit_str(value: "assignee="), op: "+", right: ts_ident(name: "assignee")), ts_binop(left: ts_lit_str(value: "csrf="), op: "+", right: ts_member(object: ts_ident(name: "context"), property: "csrf")), ts_binop(left: ts_lit_str(value: "expected_revision="), op: "+", right: ts_member(object: ts_ident(name: "context"), property: "expected_revision"))]), property: "join"), args: [ts_call(callee: ts_member(object: ts_ident(name: "String"), property: "fromCharCode"), args: [ts_lit_num(text: "13"), ts_lit_num(text: "10")])])), - ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "fetch"), args: [ts_binop(left: ts_ident(name: "path"), op: "+", right: ts_lit_str(value: "/assign")), ts_object(fields: [ts_field(key: "method", value: ts_lit_str(value: "POST")), ts_field(key: "credentials", value: ts_lit_str(value: "same-origin")), ts_field(key: "headers", value: ts_object(fields: [ts_quoted_field(key: "Content-Type", value: ts_lit_str(value: "text/plain;charset=UTF-8"))])), ts_field(key: "body", value: ts_ident(name: "body"))])]), property: "then"), args: [ts_arrow(params: ["response"], body: ts_block(stmts: [ + ts_const(name: "body", value: ts_call(callee: ts_member(object: ts_new(callee: ts_ident(name: "URLSearchParams"), args: [ts_object(fields: [ts_field(key: "key", value: ts_member(object: ts_ident(name: "context"), property: "key")), ts_field(key: "assignee", value: ts_ident(name: "assignee")), ts_field(key: "csrf", value: ts_member(object: ts_ident(name: "context"), property: "csrf")), ts_field(key: "expected_revision", value: ts_member(object: ts_ident(name: "context"), property: "expected_revision"))])]), property: "toString"), args: [])), + ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "fetch"), args: [ts_binop(left: ts_ident(name: "path"), op: "+", right: ts_lit_str(value: "/assign")), ts_object(fields: [ts_field(key: "method", value: ts_lit_str(value: "POST")), ts_field(key: "credentials", value: ts_lit_str(value: "same-origin")), ts_field(key: "headers", value: ts_object(fields: [ts_quoted_field(key: "Content-Type", value: ts_lit_str(value: "application/x-www-form-urlencoded;charset=UTF-8"))])), ts_field(key: "body", value: ts_ident(name: "body"))])]), property: "then"), args: [ts_arrow(params: ["response"], body: ts_block(stmts: [ ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "response"), property: "text"), args: []), property: "then"), args: [ts_arrow(params: ["message"], body: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "result"), property: "textContent"), op: "=", right: ts_binop(left: ts_ident(name: "message"), op: "||", right: ts_binop(left: ts_binop(left: ts_lit_str(value: "Assignment returned HTTP "), op: "+", right: ts_member(object: ts_ident(name: "response"), property: "status")), op: "+", right: ts_lit_str(value: ". Reload to check the issue before retrying."))))), ts_return(expr: Present { value: ts_binop(left: ts_member(object: ts_ident(name: "response"), property: "ok"), op: "||", right: ts_call(callee: ts_member(object: ts_ident(name: "message"), property: "startsWith"), args: [ts_lit_str(value: "assigned:")])) }) @@ -59,7 +59,8 @@ fn inline_assignment_bind_statements() -> List { ts_const(name: "path", value: ts_binop(left: ts_lit_str(value: "/issue/"), op: "+", right: ts_call(callee: ts_ident(name: "encodeURIComponent"), args: [ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "assignNode")]))), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "disabled"), op: "=", right: ts_ident(name: "true"))), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "title"), op: "=", right: ts_lit_str(value: "Loading assignees…"))), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "assignmentContext"), args: [ts_ident(name: "path")]), property: "then"), args: [ts_arrow(params: ["context"], body: ts_block(stmts: [ + ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_cond(test: ts_member(object: ts_ident(name: "button"), property: "assignmentPendingContext"), consequent: ts_call(callee: ts_member(object: ts_ident(name: "Promise"), property: "resolve"), args: [ts_member(object: ts_ident(name: "button"), property: "assignmentPendingContext")]), alternate: ts_call(callee: ts_ident(name: "assignmentContext"), args: [ts_ident(name: "path")])), property: "then"), args: [ts_arrow(params: ["context"], body: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "assignmentPendingContext"), op: "=", right: ts_ident(name: "context"))), ts_const(name: "select", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "select")])), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "select"), property: "className"), op: "=", right: ts_lit_str(value: "issue-inline-picker"))), ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "select"), property: "setAttribute"), args: [ts_lit_str(value: "aria-label"), ts_lit_str(value: "Choose assignee")])), @@ -71,7 +72,7 @@ fn inline_assignment_bind_statements() -> List { ts_const(name: "option", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "option")])), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "option"), property: "value"), op: "=", right: ts_member(object: ts_ident(name: "choice"), property: "value"))), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "option"), property: "textContent"), op: "=", right: ts_member(object: ts_ident(name: "choice"), property: "label"))), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "option"), property: "disabled"), op: "=", right: ts_binop(left: ts_member(object: ts_ident(name: "choice"), property: "disabled"), op: "===", right: ts_ident(name: "true")))), + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "option"), property: "disabled"), op: "=", right: ts_binop(left: ts_binop(left: ts_member(object: ts_ident(name: "choice"), property: "disabled"), op: "===", right: ts_ident(name: "true")), op: "||", right: ts_binop(left: ts_member(object: ts_ident(name: "context"), property: "pendingAssignee"), op: "&&", right: ts_binop(left: ts_member(object: ts_ident(name: "context"), property: "pendingAssignee"), op: "!==", right: ts_member(object: ts_ident(name: "choice"), property: "value")))))), ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "select"), property: "appendChild"), args: [ts_ident(name: "option")])) ]))])), ts_let(name: "closed", value: ts_ident(name: "false")), @@ -100,12 +101,14 @@ fn inline_assignment_bind_statements() -> List { ts_if(cond: ts_prefix(op: "!", expr: ts_member(object: ts_ident(name: "select"), property: "value")), then: ts_block(stmts: [ ts_return(expr: none) ]), else_stmt: none), + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "context"), property: "pendingAssignee"), op: "=", right: ts_member(object: ts_ident(name: "select"), property: "value"))), ts_const(name: "choice", value: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "context"), property: "options"), property: "find"), args: [ts_arrow(params: ["option"], body: ts_block(stmts: [ ts_return(expr: Present { value: ts_binop(left: ts_member(object: ts_ident(name: "option"), property: "value"), op: "===", right: ts_member(object: ts_ident(name: "select"), property: "value")) }) ]))])), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "select"), property: "disabled"), op: "=", right: ts_ident(name: "true"))), ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "assignmentPost"), args: [ts_ident(name: "path"), ts_ident(name: "context"), ts_member(object: ts_ident(name: "select"), property: "value"), ts_ident(name: "row")]), property: "then"), args: [ts_arrow(params: ["recorded"], body: ts_block(stmts: [ ts_if(cond: ts_ident(name: "recorded"), then: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "assignmentPendingContext"), op: "=", right: ts_ident(name: "null"))), ts_if(cond: ts_member(object: ts_ident(name: "choice"), property: "chip_html"), then: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "innerHTML"), op: "=", right: ts_member(object: ts_ident(name: "choice"), property: "chip_html"))) ]), else_stmt: Present { value: ts_block(stmts: [ @@ -115,7 +118,7 @@ fn inline_assignment_bind_statements() -> List { ]), else_stmt: none), ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "close"), args: [])) ]))]), property: "catch"), args: [ts_arrow(params: ["error"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_call(callee: ts_ident(name: "assignmentResult"), args: [ts_ident(name: "row")]), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_lit_str(value: "Could not confirm assignment: "), op: "+", right: ts_member(object: ts_ident(name: "error"), property: "message")), op: "+", right: ts_lit_str(value: ". Check the issue before retrying.")))), + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_call(callee: ts_ident(name: "assignmentResult"), args: [ts_ident(name: "row")]), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_lit_str(value: "Could not confirm assignment: "), op: "+", right: ts_member(object: ts_ident(name: "error"), property: "message")), op: "+", right: ts_lit_str(value: ". Retry the same assignment to confirm its original result; reload before changing it.")))), ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "close"), args: [])) ]))])) ]))])), @@ -141,12 +144,13 @@ fn inline_assignment_bind_statements() -> List { ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "submit"), property: "disabled"), op: "=", right: ts_ident(name: "true"))), ts_const(name: "path", value: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "form"), property: "getAttribute"), args: [ts_lit_str(value: "action")]), property: "slice"), args: [ts_lit_num(text: "0"), ts_prefix(op: "-", expr: ts_lit_num(text: "7"))])), ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "assignmentContext"), args: [ts_ident(name: "path")]), property: "then"), args: [ts_arrow(params: ["context"], body: ts_block(stmts: [ + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "context"), property: "key"), op: "=", right: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "form"), property: "querySelector"), args: [ts_lit_str(value: "[name=\"key\"]")]), property: "value"))), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "context"), property: "expected_revision"), op: "=", right: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "form"), property: "querySelector"), args: [ts_lit_str(value: "[name=\"expected_revision\"]")]), property: "value"))), ts_return(expr: Present { value: ts_call(callee: ts_ident(name: "assignmentPost"), args: [ts_ident(name: "path"), ts_ident(name: "context"), ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "form"), property: "querySelector"), args: [ts_lit_str(value: "[name=\"assignee\"]")]), property: "value"), ts_ident(name: "form")]) }) ]))]), property: "then"), args: [ts_arrow(params: [], body: ts_block(stmts: [ ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "submit"), property: "disabled"), op: "=", right: ts_ident(name: "false"))) ]))]), property: "catch"), args: [ts_arrow(params: ["error"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_call(callee: ts_ident(name: "assignmentResult"), args: [ts_ident(name: "form")]), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_lit_str(value: "Could not confirm assignment: "), op: "+", right: ts_member(object: ts_ident(name: "error"), property: "message")), op: "+", right: ts_lit_str(value: ". Check the issue before retrying.")))), + ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_call(callee: ts_ident(name: "assignmentResult"), args: [ts_ident(name: "form")]), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_lit_str(value: "Could not confirm assignment: "), op: "+", right: ts_member(object: ts_ident(name: "error"), property: "message")), op: "+", right: ts_lit_str(value: ". Retry the same assignment to confirm its original result; reload before changing it.")))), ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "submit"), property: "disabled"), op: "=", right: ts_ident(name: "false"))) ]))])) ]))])) diff --git a/dag/gunbc/roadmap/roadmap_issue_command.dag b/dag/gunbc/roadmap/roadmap_issue_command.dag new file mode 100644 index 00000000000..6b0500fac15 --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_issue_command.dag @@ -0,0 +1,47 @@ +module gunbc.roadmap_issue_command + +import std.types { String, NonEmptyStr } +import std.algebra { trim } +import extdeps.entropy { Urandom } +import std.encoding { base64_decode, Standard } +import gunbc.roadmap_model { RoadmapNodeId } +import gunbc.roadmap.roadmap_event_log { EventPrincipal, RoadmapEventId, RoadmapEvent, Claimed, CommentCreated } +import gunbc.roadmap.roadmap_event_carrier { RoadmapEventCarrierLayout, RoadmapEventAppend, roadmap_event_append } + +// Authorization precedes this boundary. The operation is a submission identity, independent of +// comment text or the observation clock. Native forms receive it before submission, just as JS does. +type IssueCommandSubject { + node: RoadmapNodeId + author: EventPrincipal + operation: NonEmptyStr + expected: RoadmapEventId? +} + +type IssueCommand + = AssignIssue { subject: IssueCommandSubject, assignee: EventPrincipal, return_to: EventPrincipal } + | CreateComment { subject: IssueCommandSubject, body: NonEmptyStr } + +type IssueFormContext { operation: String, csrf: String } + +fn issue_operation_nonce() -> String? { + let wire = trim(Urandom.ReadBytes(count: 32).octets_b64) + match base64_decode(s: wire, variant: Standard) { + Absent => none + Present { value: bytes } => if count(bytes) != 32 { none } + else { Present { value: replace(replace(replace(wire, "+", "-"), "/", "_"), "=", "") } } + } +} + +fn issue_command_event(command: IssueCommand, recorded_at: String) -> RoadmapEvent { + match command { + AssignIssue { subject, assignee, return_to } => RoadmapEvent { node: subject.node, author: subject.author, + parent: subject.expected, kind: Claimed { assignee: assignee, return_to: return_to }, recorded_at: recorded_at } + CreateComment { subject, body } => RoadmapEvent { node: subject.node, author: subject.author, + parent: subject.expected, kind: CommentCreated { comment: subject.operation, body: body, visibility: "public", parent_comment: none }, recorded_at: recorded_at } + } +} + +fn issue_command_apply(layout: RoadmapEventCarrierLayout, command: IssueCommand, recorded_at: String) -> RoadmapEventAppend { + let operation = match command { AssignIssue { subject, assignee: _, return_to: _ } => subject.operation CreateComment { subject, body: _ } => subject.operation } + roadmap_event_append(layout: layout, event: issue_command_event(command: command, recorded_at: recorded_at), operation: operation as String) +} diff --git a/dag/gunbc/roadmap/roadmap_issue_sort.dag b/dag/gunbc/roadmap/roadmap_issue_sort.dag index 8c53b3308e4..1c9cb22cef4 100644 --- a/dag/gunbc/roadmap/roadmap_issue_sort.dag +++ b/dag/gunbc/roadmap/roadmap_issue_sort.dag @@ -6,132 +6,260 @@ import extdeps.languages.typescript.program { ts_new, ts_cond, ts_prefix, ts_object, ts_field, ts_array, ts_binop, ts_for_of, } -// Presentation-only ordering over the server's modeled sort values and dependency edges. -// Stable Kahn traversal chooses the first eligible row in the preceding order. Missing -// prerequisites are outside this table; cycles refuse before any DOM mutation. All rows, -// including filtered rows, participate. Undo restores order and sort state together. +// Stable dependency ordering and recursive project focus use only server-projected identities. +// Undo restores order, focus and expansion together. Unknown or cyclic nesting never invents a project. fn issue_sort_bind_statements() -> List { - [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), args: [ts_lit_str(value: ".roadmap-frontier")]), property: "forEach"), args: [ts_arrow(params: ["root"], body: ts_block(stmts: [ - ts_const(name: "table", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-table")])), - ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "table")), then: ts_block(stmts: [ - ts_return(expr: none) - ]), else_stmt: none), - ts_const(name: "list", value: ts_call(callee: ts_member(object: ts_ident(name: "table"), property: "querySelector"), args: [ts_lit_str(value: "ul")])), - ts_const(name: "buttons", value: ts_call(callee: ts_member(object: ts_ident(name: "Array"), property: "from"), args: [ts_call(callee: ts_member(object: ts_ident(name: "table"), property: "querySelectorAll"), args: [ts_lit_str(value: "[data-sort-key]")])])), - ts_const(name: "topological", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-topological")])), - ts_const(name: "undo", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-sort-undo")])), - ts_const(name: "status", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-sort-status")])), - ts_const(name: "history", value: ts_array(elements: [])), - ts_let(name: "key", value: ts_lit_str(value: "")), - ts_let(name: "direction", value: ts_lit_str(value: "ascending")), - ts_let(name: "topo", value: ts_ident(name: "false")), - ts_const(name: "rows", value: ts_arrow(params: [], body: ts_block(stmts: [ - ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_ident(name: "Array"), property: "from"), args: [ts_member(object: ts_ident(name: "list"), property: "children")]) }) - ]))), - ts_const(name: "paint", value: ts_arrow(params: [], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "buttons"), property: "forEach"), args: [ts_arrow(params: ["button"], body: ts_block(stmts: [ - ts_const(name: "label", value: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel"), op: "||", right: ts_member(object: ts_ident(name: "button"), property: "textContent"))), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel"), op: "=", right: ts_ident(name: "label"))), - ts_const(name: "selected", value: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "topo")), op: "&&", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortKey"), op: "===", right: ts_ident(name: "key")))), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "parentElement"), property: "setAttribute"), args: [ts_lit_str(value: "aria-sort"), ts_cond(test: ts_ident(name: "selected"), consequent: ts_ident(name: "direction"), alternate: ts_lit_str(value: "none"))])), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "textContent"), op: "=", right: ts_binop(left: ts_ident(name: "label"), op: "+", right: ts_cond(test: ts_ident(name: "selected"), consequent: ts_cond(test: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending")), consequent: ts_lit_str(value: " ↑"), alternate: ts_lit_str(value: " ↓")), alternate: ts_lit_str(value: ""))))), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "button"), property: "setAttribute"), args: [ts_lit_str(value: "aria-label"), ts_binop(left: ts_binop(left: ts_lit_str(value: "Sort by "), op: "+", right: ts_ident(name: "label")), op: "+", right: ts_cond(test: ts_ident(name: "selected"), consequent: ts_cond(test: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending")), consequent: ts_lit_str(value: ", descending next"), alternate: ts_lit_str(value: ", ascending next")), alternate: ts_lit_str(value: ", ascending next")))])) - ]))])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "topological"), property: "setAttribute"), args: [ts_lit_str(value: "aria-pressed"), ts_cond(test: ts_ident(name: "topo"), consequent: ts_lit_str(value: "true"), alternate: ts_lit_str(value: "false"))])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "topological"), property: "classList"), property: "toggle"), args: [ts_lit_str(value: "chip-on"), ts_ident(name: "topo")])), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "undo"), property: "disabled"), op: "=", right: ts_binop(left: ts_member(object: ts_ident(name: "history"), property: "length"), op: "===", right: ts_lit_num(text: "0")))) - ]))), - ts_const(name: "remember", value: ts_arrow(params: [], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "history"), property: "push"), args: [ts_object(fields: [ts_field(key: "rows", value: ts_call(callee: ts_ident(name: "rows"), args: [])), ts_field(key: "key", value: ts_ident(name: "key")), ts_field(key: "direction", value: ts_ident(name: "direction")), ts_field(key: "topo", value: ts_ident(name: "topo"))])])) - ]))), - ts_const(name: "restore", value: ts_arrow(params: [], body: ts_block(stmts: [ - ts_const(name: "previous", value: ts_call(callee: ts_member(object: ts_ident(name: "history"), property: "pop"), args: [])), - ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "previous")), then: ts_block(stmts: [ - ts_return(expr: none) - ]), else_stmt: none), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "previous"), property: "rows"), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "list"), property: "appendChild"), args: [ts_ident(name: "row")])) - ]))])), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "key"))), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "direction"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "direction"))), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "topo"))), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Previous order restored."))) - ]))), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "undo"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_ident(name: "restore")])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "buttons"), property: "forEach"), args: [ts_arrow(params: ["button"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "button"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ - ts_const(name: "nextKey", value: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortKey")), - ts_const(name: "nextDirection", value: ts_cond(test: ts_binop(left: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "topo")), op: "&&", right: ts_binop(left: ts_ident(name: "key"), op: "===", right: ts_ident(name: "nextKey"))), op: "&&", right: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending"))), consequent: ts_lit_str(value: "descending"), alternate: ts_lit_str(value: "ascending"))), - ts_const(name: "ordered", value: ts_call(callee: ts_ident(name: "rows"), args: [])), - ts_const(name: "value", value: ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_const(name: "cell", value: ts_call(callee: ts_member(object: ts_ident(name: "row"), property: "querySelector"), args: [ts_binop(left: ts_binop(left: ts_lit_str(value: "[data-col=\""), op: "+", right: ts_ident(name: "nextKey")), op: "+", right: ts_lit_str(value: "\"]"))])), - ts_return(expr: Present { value: ts_cond(test: ts_ident(name: "cell"), consequent: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "cell"), property: "dataset"), property: "sortValue"), op: "||", right: ts_lit_str(value: "")), alternate: ts_lit_str(value: "")) }) - ]))), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "sort"), args: [ts_arrow(params: ["a", "b"], body: ts_block(stmts: [ - ts_const(name: "av", value: ts_call(callee: ts_ident(name: "value"), args: [ts_ident(name: "a")])), - ts_const(name: "bv", value: ts_call(callee: ts_ident(name: "value"), args: [ts_ident(name: "b")])), - ts_if(cond: ts_binop(left: ts_ident(name: "av"), op: "===", right: ts_lit_str(value: "")), then: ts_block(stmts: [ - ts_return(expr: Present { value: ts_cond(test: ts_binop(left: ts_ident(name: "bv"), op: "===", right: ts_lit_str(value: "")), consequent: ts_lit_num(text: "0"), alternate: ts_lit_num(text: "1")) }) - ]), else_stmt: none), - ts_if(cond: ts_binop(left: ts_ident(name: "bv"), op: "===", right: ts_lit_str(value: "")), then: ts_block(stmts: [ - ts_return(expr: Present { value: ts_prefix(op: "-", expr: ts_lit_num(text: "1")) }) - ]), else_stmt: none), - ts_const(name: "left", value: ts_cond(test: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortNumeric"), op: "===", right: ts_lit_str(value: "true")), consequent: ts_call(callee: ts_ident(name: "Number"), args: [ts_ident(name: "av")]), alternate: ts_call(callee: ts_member(object: ts_ident(name: "av"), property: "toLowerCase"), args: []))), - ts_const(name: "right", value: ts_cond(test: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortNumeric"), op: "===", right: ts_lit_str(value: "true")), consequent: ts_call(callee: ts_ident(name: "Number"), args: [ts_ident(name: "bv")]), alternate: ts_call(callee: ts_member(object: ts_ident(name: "bv"), property: "toLowerCase"), args: []))), - ts_const(name: "compared", value: ts_cond(test: ts_binop(left: ts_ident(name: "left"), op: "<", right: ts_ident(name: "right")), consequent: ts_prefix(op: "-", expr: ts_lit_num(text: "1")), alternate: ts_cond(test: ts_binop(left: ts_ident(name: "left"), op: ">", right: ts_ident(name: "right")), consequent: ts_lit_num(text: "1"), alternate: ts_lit_num(text: "0")))), - ts_return(expr: Present { value: ts_cond(test: ts_binop(left: ts_ident(name: "nextDirection"), op: "===", right: ts_lit_str(value: "ascending")), consequent: ts_ident(name: "compared"), alternate: ts_prefix(op: "-", expr: ts_ident(name: "compared"))) }) - ]))])), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "list"), property: "appendChild"), args: [ts_ident(name: "row")])) - ]))])), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_ident(name: "nextKey"))), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "direction"), op: "=", right: ts_ident(name: "nextDirection"))), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "false"))), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_binop(left: ts_binop(left: ts_lit_str(value: "Sorted by "), op: "+", right: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel")), op: "+", right: ts_lit_str(value: ", ")), op: "+", right: ts_ident(name: "direction")), op: "+", right: ts_lit_str(value: ".")))) - ]))])) - ]))])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "topological"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ - ts_if(cond: ts_ident(name: "topo"), then: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "restore"), args: [])), - ts_return(expr: none) - ]), else_stmt: none), - ts_const(name: "original", value: ts_call(callee: ts_ident(name: "rows"), args: [])), - ts_let(name: "remaining", value: ts_call(callee: ts_member(object: ts_ident(name: "original"), property: "slice"), args: [])), - ts_const(name: "ordered", value: ts_array(elements: [])), - ts_for_of(var_name: "step", iterable: ts_ident(name: "original"), body: ts_block(stmts: [ - ts_const(name: "next", value: ts_call(callee: ts_member(object: ts_ident(name: "remaining"), property: "find"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_const(name: "dependencies", value: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "dependencies"), op: "||", right: ts_lit_str(value: "")), property: "split"), args: [ts_lit_str(value: " ")]), property: "filter"), args: [ts_arrow(params: ["id"], body: ts_block(stmts: [ - ts_return(expr: Present { value: ts_binop(left: ts_ident(name: "id"), op: "!==", right: ts_lit_str(value: "")) }) - ]))])), - ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_ident(name: "dependencies"), property: "every"), args: [ts_arrow(params: ["id"], body: ts_block(stmts: [ - ts_return(expr: Present { value: ts_prefix(op: "!", expr: ts_call(callee: ts_member(object: ts_ident(name: "remaining"), property: "some"), args: [ts_arrow(params: ["candidate"], body: ts_block(stmts: [ - ts_return(expr: Present { value: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "candidate"), property: "dataset"), property: "node"), op: "===", right: ts_ident(name: "id")) }) - ]))])) }) - ]))]) }) - ]))])), - ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "next")), then: ts_block(stmts: [ - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Cannot sort: dependency cycle. Order unchanged."))), - ts_return(expr: none) - ]), else_stmt: none), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "push"), args: [ts_ident(name: "next")])), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "remaining"), op: "=", right: ts_call(callee: ts_member(object: ts_ident(name: "remaining"), property: "filter"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_return(expr: Present { value: ts_binop(left: ts_ident(name: "row"), op: "!==", right: ts_ident(name: "next")) }) - ]))]))) - ])), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ - ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "list"), property: "appendChild"), args: [ts_ident(name: "row")])) - ]))])), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_lit_str(value: ""))), - ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "true"))), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), - ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Prerequisites before dependents."))) - ]))])), - ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])) - ]))])) - ] +[ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "querySelectorAll"), args: [ts_lit_str(value: ".roadmap-frontier")]), property: "forEach"), args: [ts_arrow(params: ["root"], body: ts_block(stmts: [ +ts_const(name: "table", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-table")])), +ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "table")), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_const(name: "list", value: ts_call(callee: ts_member(object: ts_ident(name: "table"), property: "querySelector"), args: [ts_lit_str(value: "ul")])), +ts_const(name: "buttons", value: ts_call(callee: ts_member(object: ts_ident(name: "Array"), property: "from"), args: [ts_call(callee: ts_member(object: ts_ident(name: "table"), property: "querySelectorAll"), args: [ts_lit_str(value: "[data-sort-key]")])])), +ts_const(name: "topological", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-topological")])), +ts_const(name: "undo", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-sort-undo")])), +ts_const(name: "status", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: ".issue-sort-status")])), +ts_if(cond: ts_binop(left: ts_binop(left: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "list")), op: "||", right: ts_prefix(op: "!", expr: ts_ident(name: "topological"))), op: "||", right: ts_prefix(op: "!", expr: ts_ident(name: "undo"))), op: "||", right: ts_prefix(op: "!", expr: ts_ident(name: "status"))), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_const(name: "rows", value: ts_arrow(params: [], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_ident(name: "Array"), property: "from"), args: [ts_member(object: ts_ident(name: "list"), property: "children")]) }) +]))), +ts_const(name: "initial", value: ts_call(callee: ts_ident(name: "rows"), args: [])), +ts_const(name: "history", value: ts_array(elements: [])), +ts_let(name: "key", value: ts_lit_str(value: "")), +ts_let(name: "direction", value: ts_lit_str(value: "ascending")), +ts_let(name: "topo", value: ts_ident(name: "false")), +ts_let(name: "project", value: ts_lit_str(value: "")), +ts_let(name: "selected", value: ts_lit_str(value: "")), +ts_let(name: "expanded", value: ts_ident(name: "false")), +ts_const(name: "controls", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "span")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "controls"), property: "className"), op: "=", right: ts_lit_str(value: "issue-focus-controls"))), +ts_const(name: "back", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "button")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "back"), property: "type"), op: "=", right: ts_lit_str(value: "button"))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "back"), property: "textContent"), op: "=", right: ts_lit_str(value: "All issues"))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "back"), property: "className"), op: "=", right: ts_lit_str(value: "chip"))), +ts_const(name: "more", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "button")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "more"), property: "type"), op: "=", right: ts_lit_str(value: "button"))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "more"), property: "className"), op: "=", right: ts_lit_str(value: "chip"))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "more"), property: "setAttribute"), args: [ts_lit_str(value: "aria-label"), ts_lit_str(value: "Expand earlier completed work")])), +ts_const(name: "detail", value: ts_call(callee: ts_member(object: ts_ident(name: "document"), property: "createElement"), args: [ts_lit_str(value: "a")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "detail"), property: "textContent"), op: "=", right: ts_lit_str(value: "Open selected issue"))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "detail"), property: "className"), op: "=", right: ts_lit_str(value: "chip"))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "controls"), property: "appendChild"), args: [ts_ident(name: "back")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "controls"), property: "appendChild"), args: [ts_ident(name: "more")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "controls"), property: "appendChild"), args: [ts_ident(name: "detail")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "status"), property: "parentElement"), property: "appendChild"), args: [ts_ident(name: "controls")])), +ts_const(name: "paint", value: ts_arrow(params: [], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "buttons"), property: "forEach"), args: [ts_arrow(params: ["button"], body: ts_block(stmts: [ +ts_const(name: "label", value: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel"), op: "||", right: ts_member(object: ts_ident(name: "button"), property: "textContent"))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel"), op: "=", right: ts_ident(name: "label"))), +ts_const(name: "active", value: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "topo")), op: "&&", right: ts_binop(left: ts_ident(name: "key"), op: "===", right: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortKey")))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "parentElement"), property: "setAttribute"), args: [ts_lit_str(value: "aria-sort"), ts_cond(test: ts_ident(name: "active"), consequent: ts_ident(name: "direction"), alternate: ts_lit_str(value: "none"))])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "button"), property: "textContent"), op: "=", right: ts_binop(left: ts_ident(name: "label"), op: "+", right: ts_cond(test: ts_ident(name: "active"), consequent: ts_cond(test: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending")), consequent: ts_lit_str(value: " ↑"), alternate: ts_lit_str(value: " ↓")), alternate: ts_lit_str(value: ""))))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "button"), property: "setAttribute"), args: [ts_lit_str(value: "aria-label"), ts_binop(left: ts_binop(left: ts_lit_str(value: "Sort by "), op: "+", right: ts_ident(name: "label")), op: "+", right: ts_cond(test: ts_binop(left: ts_ident(name: "active"), op: "&&", right: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending"))), consequent: ts_lit_str(value: ", descending next"), alternate: ts_lit_str(value: ", ascending next")))])) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "topological"), property: "setAttribute"), args: [ts_lit_str(value: "aria-pressed"), ts_cond(test: ts_ident(name: "topo"), consequent: ts_lit_str(value: "true"), alternate: ts_lit_str(value: "false"))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "topological"), property: "classList"), property: "toggle"), args: [ts_lit_str(value: "chip-on"), ts_ident(name: "topo")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "undo"), property: "disabled"), op: "=", right: ts_binop(left: ts_member(object: ts_ident(name: "history"), property: "length"), op: "===", right: ts_lit_num(text: "0")))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "controls"), property: "hidden"), op: "=", right: ts_prefix(op: "!", expr: ts_ident(name: "project")))), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "detail"), property: "href"), op: "=", right: ts_binop(left: ts_lit_str(value: "/issue/"), op: "+", right: ts_call(callee: ts_ident(name: "encodeURIComponent"), args: [ts_ident(name: "selected")])))), +ts_const(name: "completed", value: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "filter"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project"), op: "===", right: ts_ident(name: "project")), op: "&&", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "complete"), op: "===", right: ts_lit_str(value: "true"))) }) +]))])), +ts_const(name: "recent", value: ts_call(callee: ts_member(object: ts_ident(name: "completed"), property: "slice"), args: [ts_prefix(op: "-", expr: ts_lit_num(text: "3"))])), +ts_const(name: "earlier", value: ts_call(callee: ts_member(object: ts_ident(name: "completed"), property: "filter"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_prefix(op: "!", expr: ts_call(callee: ts_member(object: ts_ident(name: "recent"), property: "includes"), args: [ts_ident(name: "row")])), op: "&&", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "node"), op: "!==", right: ts_ident(name: "selected"))) }) +]))])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "more"), property: "hidden"), op: "=", right: ts_binop(left: ts_member(object: ts_ident(name: "earlier"), property: "length"), op: "===", right: ts_lit_num(text: "0")))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "more"), property: "style"), property: "setProperty"), args: [ts_lit_str(value: "display"), ts_cond(test: ts_binop(left: ts_member(object: ts_ident(name: "earlier"), property: "length"), op: "===", right: ts_lit_num(text: "0")), consequent: ts_lit_str(value: "none"), alternate: ts_lit_str(value: "")), ts_lit_str(value: "important")])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "more"), property: "textContent"), op: "=", right: ts_cond(test: ts_ident(name: "expanded"), consequent: ts_lit_str(value: "Collapse earlier work"), alternate: ts_binop(left: ts_binop(left: ts_lit_str(value: "… "), op: "+", right: ts_member(object: ts_ident(name: "earlier"), property: "length")), op: "+", right: ts_lit_str(value: " earlier"))))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "more"), property: "setAttribute"), args: [ts_lit_str(value: "aria-expanded"), ts_cond(test: ts_ident(name: "expanded"), consequent: ts_lit_str(value: "true"), alternate: ts_lit_str(value: "false"))])), +ts_const(name: "search", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: "input.issue-search")])), +ts_const(name: "needle", value: ts_cond(test: ts_ident(name: "search"), consequent: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "search"), property: "value"), property: "toLowerCase"), args: []), alternate: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_const(name: "inProject", value: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "project")), op: "||", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project"), op: "===", right: ts_ident(name: "project")))), +ts_const(name: "inWindow", value: ts_binop(left: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "project")), op: "||", right: ts_ident(name: "expanded")), op: "||", right: ts_prefix(op: "!", expr: ts_call(callee: ts_member(object: ts_ident(name: "earlier"), property: "includes"), args: [ts_ident(name: "row")])))), +ts_const(name: "inSearch", value: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "search"), op: "||", right: ts_member(object: ts_ident(name: "row"), property: "textContent")), property: "toLowerCase"), args: []), property: "includes"), args: [ts_ident(name: "needle")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "style"), property: "setProperty"), args: [ts_lit_str(value: "display"), ts_cond(test: ts_binop(left: ts_binop(left: ts_ident(name: "inProject"), op: "&&", right: ts_ident(name: "inWindow")), op: "&&", right: ts_ident(name: "inSearch")), consequent: ts_lit_str(value: ""), alternate: ts_lit_str(value: "none")), ts_lit_str(value: "important")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "classList"), property: "toggle"), args: [ts_lit_str(value: "issue-focus-selected"), ts_binop(left: ts_prefix(op: "!", expr: ts_prefix(op: "!", expr: ts_ident(name: "project"))), op: "&&", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "node"), op: "===", right: ts_ident(name: "selected")))])) +]))])) +]))), +ts_const(name: "remember", value: ts_arrow(params: [], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "history"), property: "push"), args: [ts_object(fields: [ts_field(key: "rows", value: ts_call(callee: ts_ident(name: "rows"), args: [])), ts_field(key: "key", value: ts_ident(name: "key")), ts_field(key: "direction", value: ts_ident(name: "direction")), ts_field(key: "topo", value: ts_ident(name: "topo")), ts_field(key: "project", value: ts_ident(name: "project")), ts_field(key: "selected", value: ts_ident(name: "selected")), ts_field(key: "expanded", value: ts_ident(name: "expanded"))])])) +]))), +ts_const(name: "place", value: ts_arrow(params: ["ordered"], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "list"), property: "appendChild"), args: [ts_ident(name: "row")])) +]))])) +]))), +ts_const(name: "restore", value: ts_arrow(params: [], body: ts_block(stmts: [ +ts_const(name: "previous", value: ts_call(callee: ts_member(object: ts_ident(name: "history"), property: "pop"), args: [])), +ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "previous")), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_member(object: ts_ident(name: "previous"), property: "rows")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "key"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "direction"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "direction"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "topo"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "project"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "project"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "selected"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "selected"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_member(object: ts_ident(name: "previous"), property: "expanded"))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Previous order and project view restored."))) +]))), +ts_const(name: "orderedTopologically", value: ts_arrow(params: ["scope"], body: ts_block(stmts: [ +ts_const(name: "pending", value: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "filter"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "scope")), op: "||", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project"), op: "===", right: ts_ident(name: "scope"))) }) +]))])), +ts_const(name: "ids", value: ts_new(callee: ts_ident(name: "Set"), args: [ts_call(callee: ts_member(object: ts_ident(name: "pending"), property: "map"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "node") }) +]))])])), +ts_const(name: "emitted", value: ts_new(callee: ts_ident(name: "Set"), args: [])), +ts_const(name: "ordered", value: ts_array(elements: [])), +ts_for_of(var_name: "unused", iterable: ts_ident(name: "pending"), body: ts_block(stmts: [ +ts_const(name: "next", value: ts_call(callee: ts_member(object: ts_ident(name: "pending"), property: "find"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_prefix(op: "!", expr: ts_call(callee: ts_member(object: ts_ident(name: "emitted"), property: "has"), args: [ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "node")])), op: "&&", right: ts_call(callee: ts_member(object: ts_call(callee: ts_member(object: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "dependencies"), op: "||", right: ts_lit_str(value: "")), property: "split"), args: [ts_lit_str(value: " ")]), property: "every"), args: [ts_arrow(params: ["id"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "id")), op: "||", right: ts_prefix(op: "!", expr: ts_call(callee: ts_member(object: ts_ident(name: "ids"), property: "has"), args: [ts_ident(name: "id")]))), op: "||", right: ts_call(callee: ts_member(object: ts_ident(name: "emitted"), property: "has"), args: [ts_ident(name: "id")])) }) +]))])) }) +]))])), +ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "next")), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Cannot order cyclic dependencies; the current view is unchanged."))), +ts_return(expr: Present { value: ts_ident(name: "null") }) +]), else_stmt: none), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "push"), args: [ts_ident(name: "next")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "emitted"), property: "add"), args: [ts_member(object: ts_member(object: ts_ident(name: "next"), property: "dataset"), property: "node")])) +])), +ts_return(expr: Present { value: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "concat"), args: [ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "filter"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_prefix(op: "!", expr: ts_prefix(op: "!", expr: ts_ident(name: "scope"))), op: "&&", right: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project"), op: "!==", right: ts_ident(name: "scope"))) }) +]))])]) }) +]))), +ts_const(name: "focus", value: ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_if(cond: ts_prefix(op: "!", expr: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project")), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Project nesting is ambiguous or cyclic; no project view was selected."))), +ts_return(expr: none) +]), else_stmt: none), +ts_const(name: "ordered", value: ts_call(callee: ts_ident(name: "orderedTopologically"), args: [ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project")])), +ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "ordered")), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_ident(name: "ordered")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "project"), op: "=", right: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "project"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "selected"), op: "=", right: ts_member(object: ts_member(object: ts_ident(name: "row"), property: "dataset"), property: "node"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "true"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_const(name: "members", value: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "filter"), args: [ts_arrow(params: ["r"], body: ts_block(stmts: [ +ts_return(expr: Present { value: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "r"), property: "dataset"), property: "project"), op: "===", right: ts_ident(name: "project")) }) +]))])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_cond(test: ts_binop(left: ts_member(object: ts_ident(name: "members"), property: "length"), op: "===", right: ts_lit_num(text: "1")), consequent: ts_lit_str(value: "No nesting declared; showing this issue."), alternate: ts_binop(left: ts_binop(left: ts_lit_str(value: "Project "), op: "+", right: ts_ident(name: "project")), op: "+", right: ts_lit_str(value: ": recent completed work, then upcoming work in dependency order. Expand … for earlier work."))))), +ts_const(name: "anchor", value: ts_binop(left: ts_call(callee: ts_member(object: ts_ident(name: "row"), property: "querySelector"), args: [ts_lit_str(value: ".issue-title-link")]), op: "||", right: ts_ident(name: "row"))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "anchor"), property: "scrollIntoView"), args: [ts_object(fields: [ts_field(key: "block", value: ts_lit_str(value: "center"))])])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "anchor"), property: "focus"), args: [ts_object(fields: [ts_field(key: "preventScroll", value: ts_ident(name: "true"))])])) +]))), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_call(callee: ts_ident(name: "rows"), args: []), property: "forEach"), args: [ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "row"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: ["event"], body: ts_block(stmts: [ +ts_if(cond: ts_binop(left: ts_binop(left: ts_binop(left: ts_binop(left: ts_member(object: ts_ident(name: "event"), property: "button"), op: "||", right: ts_member(object: ts_ident(name: "event"), property: "metaKey")), op: "||", right: ts_member(object: ts_ident(name: "event"), property: "ctrlKey")), op: "||", right: ts_member(object: ts_ident(name: "event"), property: "shiftKey")), op: "||", right: ts_member(object: ts_ident(name: "event"), property: "altKey")), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_const(name: "link", value: ts_call(callee: ts_member(object: ts_member(object: ts_ident(name: "event"), property: "target"), property: "closest"), args: [ts_lit_str(value: ".issue-title-link")])), +ts_if(cond: ts_binop(left: ts_ident(name: "link"), op: "||", right: ts_binop(left: ts_member(object: ts_ident(name: "event"), property: "target"), op: "===", right: ts_ident(name: "row"))), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "event"), property: "preventDefault"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "focus"), args: [ts_ident(name: "row")])) +]), else_stmt: none) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "row"), property: "addEventListener"), args: [ts_lit_str(value: "keydown"), ts_arrow(params: ["event"], body: ts_block(stmts: [ +ts_if(cond: ts_binop(left: ts_binop(left: ts_member(object: ts_ident(name: "event"), property: "target"), op: "===", right: ts_ident(name: "row")), op: "&&", right: ts_binop(left: ts_binop(left: ts_member(object: ts_ident(name: "event"), property: "key"), op: "===", right: ts_lit_str(value: "Enter")), op: "||", right: ts_binop(left: ts_member(object: ts_ident(name: "event"), property: "key"), op: "===", right: ts_lit_str(value: " ")))), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "event"), property: "preventDefault"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "focus"), args: [ts_ident(name: "row")])) +]), else_stmt: none) +]))])) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "more"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_prefix(op: "!", expr: ts_ident(name: "expanded")))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_cond(test: ts_ident(name: "expanded"), consequent: ts_lit_str(value: "All completed project work is shown."), alternate: ts_lit_str(value: "Earlier completed work collapsed.")))) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "back"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_ident(name: "initial")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "project"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "selected"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "All issues; original order restored."))) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "undo"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_ident(name: "restore")])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "topological"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ +ts_if(cond: ts_ident(name: "topo"), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_ident(name: "initial")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "project"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "selected"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Topological mode off; original order restored."))), +ts_return(expr: none) +]), else_stmt: none), +ts_const(name: "ordered", value: ts_call(callee: ts_ident(name: "orderedTopologically"), args: [])), +ts_if(cond: ts_prefix(op: "!", expr: ts_ident(name: "ordered")), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_ident(name: "ordered")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "true"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_lit_str(value: "Prerequisites before dependents. Select a task to focus its project."))) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "buttons"), property: "forEach"), args: [ts_arrow(params: ["button"], body: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "button"), property: "addEventListener"), args: [ts_lit_str(value: "click"), ts_arrow(params: [], body: ts_block(stmts: [ +ts_const(name: "nextKey", value: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortKey")), +ts_const(name: "nextDirection", value: ts_cond(test: ts_binop(left: ts_binop(left: ts_prefix(op: "!", expr: ts_ident(name: "topo")), op: "&&", right: ts_binop(left: ts_ident(name: "key"), op: "===", right: ts_ident(name: "nextKey"))), op: "&&", right: ts_binop(left: ts_ident(name: "direction"), op: "===", right: ts_lit_str(value: "ascending"))), consequent: ts_lit_str(value: "descending"), alternate: ts_lit_str(value: "ascending"))), +ts_const(name: "value", value: ts_arrow(params: ["row"], body: ts_block(stmts: [ +ts_const(name: "cell", value: ts_call(callee: ts_member(object: ts_ident(name: "row"), property: "querySelector"), args: [ts_binop(left: ts_binop(left: ts_lit_str(value: "[data-col=\""), op: "+", right: ts_ident(name: "nextKey")), op: "+", right: ts_lit_str(value: "\"]"))])), +ts_return(expr: Present { value: ts_cond(test: ts_ident(name: "cell"), consequent: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "cell"), property: "dataset"), property: "sortValue"), op: "||", right: ts_lit_str(value: "")), alternate: ts_lit_str(value: "")) }) +]))), +ts_const(name: "ordered", value: ts_call(callee: ts_ident(name: "rows"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "ordered"), property: "sort"), args: [ts_arrow(params: ["a", "b"], body: ts_block(stmts: [ +ts_const(name: "av", value: ts_call(callee: ts_ident(name: "value"), args: [ts_ident(name: "a")])), +ts_const(name: "bv", value: ts_call(callee: ts_ident(name: "value"), args: [ts_ident(name: "b")])), +ts_if(cond: ts_binop(left: ts_ident(name: "av"), op: "===", right: ts_lit_str(value: "")), then: ts_block(stmts: [ +ts_return(expr: Present { value: ts_cond(test: ts_binop(left: ts_ident(name: "bv"), op: "===", right: ts_lit_str(value: "")), consequent: ts_lit_num(text: "0"), alternate: ts_lit_num(text: "1")) }) +]), else_stmt: none), +ts_if(cond: ts_binop(left: ts_ident(name: "bv"), op: "===", right: ts_lit_str(value: "")), then: ts_block(stmts: [ +ts_return(expr: Present { value: ts_prefix(op: "-", expr: ts_lit_num(text: "1")) }) +]), else_stmt: none), +ts_const(name: "left", value: ts_cond(test: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortNumeric"), op: "===", right: ts_lit_str(value: "true")), consequent: ts_call(callee: ts_ident(name: "Number"), args: [ts_ident(name: "av")]), alternate: ts_call(callee: ts_member(object: ts_ident(name: "av"), property: "toLowerCase"), args: []))), +ts_const(name: "right", value: ts_cond(test: ts_binop(left: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortNumeric"), op: "===", right: ts_lit_str(value: "true")), consequent: ts_call(callee: ts_ident(name: "Number"), args: [ts_ident(name: "bv")]), alternate: ts_call(callee: ts_member(object: ts_ident(name: "bv"), property: "toLowerCase"), args: []))), +ts_const(name: "compared", value: ts_cond(test: ts_binop(left: ts_ident(name: "left"), op: "<", right: ts_ident(name: "right")), consequent: ts_prefix(op: "-", expr: ts_lit_num(text: "1")), alternate: ts_cond(test: ts_binop(left: ts_ident(name: "left"), op: ">", right: ts_ident(name: "right")), consequent: ts_lit_num(text: "1"), alternate: ts_lit_num(text: "0")))), +ts_return(expr: Present { value: ts_cond(test: ts_binop(left: ts_ident(name: "nextDirection"), op: "===", right: ts_lit_str(value: "ascending")), consequent: ts_ident(name: "compared"), alternate: ts_prefix(op: "-", expr: ts_ident(name: "compared"))) }) +]))])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "remember"), args: [])), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "place"), args: [ts_ident(name: "ordered")])), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "key"), op: "=", right: ts_ident(name: "nextKey"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "direction"), op: "=", right: ts_ident(name: "nextDirection"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "topo"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "project"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "selected"), op: "=", right: ts_lit_str(value: ""))), +ts_expr_stmt(expr: ts_binop(left: ts_ident(name: "expanded"), op: "=", right: ts_ident(name: "false"))), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])), +ts_expr_stmt(expr: ts_binop(left: ts_member(object: ts_ident(name: "status"), property: "textContent"), op: "=", right: ts_binop(left: ts_binop(left: ts_binop(left: ts_binop(left: ts_lit_str(value: "Sorted by "), op: "+", right: ts_member(object: ts_member(object: ts_ident(name: "button"), property: "dataset"), property: "sortLabel")), op: "+", right: ts_lit_str(value: ", ")), op: "+", right: ts_ident(name: "direction")), op: "+", right: ts_lit_str(value: ".")))) +]))])) +]))])), +ts_const(name: "search", value: ts_call(callee: ts_member(object: ts_ident(name: "root"), property: "querySelector"), args: [ts_lit_str(value: "input.issue-search")])), +ts_if(cond: ts_ident(name: "search"), then: ts_block(stmts: [ +ts_expr_stmt(expr: ts_call(callee: ts_member(object: ts_ident(name: "search"), property: "addEventListener"), args: [ts_lit_str(value: "input"), ts_ident(name: "paint")])) +]), else_stmt: none), +ts_expr_stmt(expr: ts_call(callee: ts_ident(name: "paint"), args: [])) +]))])) +] } diff --git a/dag/gunbc/roadmap/roadmap_nesting.dag b/dag/gunbc/roadmap/roadmap_nesting.dag new file mode 100644 index 00000000000..1cc5567ee0a --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_nesting.dag @@ -0,0 +1,25 @@ +module gunbc.roadmap_nesting + +import std.types { String, List } + +// Nesting is independently authored. A blocking edge never assigns project ownership. +// Both ends name the same recursive kind of work item; no Epic/Task type hierarchy exists. +type RoadmapMembership { member: String, project: String } + +// First authored migration: shell → DAG batch 1, selected by the operator on 2026-09-28. +// The batch contract above declared_roadmap_nodes names these ten children of the typed +// invocation work item. This is a nesting declaration, not an inverse-dependency projection. +fn declared_roadmap_memberships() -> List { + [ + RoadmapMembership { member: "shell-dag-cron-entry-line-builder", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-floor-peak-calibration-rows", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-ci-fmt-gate-line", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-ci-deploy-invoke-prelude", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-ci-heal-regen-invoke", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-compile-pool-slice-install", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-slot-population-census-read", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-codex-app-server-trip-script", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-live-deploy-install-owned", project: "shell-typed-invocation" }, + RoadmapMembership { member: "shell-dag-live-deploy-restart-tailscale", project: "shell-typed-invocation" } + ] +} diff --git a/dag/gunbc/roadmap/roadmap_page.dag b/dag/gunbc/roadmap/roadmap_page.dag index af0b2b5b20b..102f1b21331 100644 --- a/dag/gunbc/roadmap/roadmap_page.dag +++ b/dag/gunbc/roadmap/roadmap_page.dag @@ -1,8 +1,13 @@ module gunbc.roadmap_page +import gunbc.roadmap_nesting { RoadmapMembership, declared_roadmap_memberships } +import gunbc.roadmap.roadmap_project { project_root_for_node } + +import gunbc.roadmap_issue_command { IssueFormContext } + import gunbc.roadmap_allocation_location { allocation_page_path } -import gunbc.roadmap.roadmap_event_log { RoadmapNodeStanding, RoadmapStandings, StandingsObserved, StandingsUnobserved, standing_for_node, roadmap_node_standing_key, roadmap_node_standing_text, NodeOpen, NodeClaimed, NodeInProgress, NodeBlocked, NodeHandedBack, NodeForked, NodeHistoryIncomplete, RoadmapEventEnvelope, roadmap_event_kind_word } +import gunbc.roadmap.roadmap_event_log { RoadmapNodeStanding, RoadmapStandings, StandingsObserved, StandingsUnobserved, standing_for_node, assignment_for_node, roadmap_issue_history_newest_first, roadmap_node_standing_key, roadmap_node_standing_text, NodeOpen, NodeClaimed, NodeInProgress, NodeBlocked, NodeHandedBack, NodeForked, NodeHistoryIncomplete, RoadmapEventEnvelope, roadmap_event_kind_word } import gunbc.roadmap.roadmap_event_log { EventPrincipal, EventPrincipalAuthenticated, LegacyEmailPrincipal, event_principal_label, IssueComment, IssueAssignmentProjection, roadmap_issue_comments, roadmap_issue_assignment, issue_comment_anchor, @@ -77,7 +82,7 @@ import gunbc.roadmap_presentation { action_priority_key, action_availability_key, SupportingFact, DisclosureView, NoDisclosure, TicketDisclosure, LegacyBodyDisclosure, - DisclosureField, DisclosureNote, + DisclosureField, DisclosureNote, TicketFieldIdentity, TicketDisplacedCost, TicketCurrentState, TicketFirstSlice, TicketRedControl, TicketOutOfScope, TicketHandback, work_scheduling_key, row_is_active_work, activity_view_from_progress, activity_line_class_key, activity_line_class_of_round, activity_summary_line, task_bar_idle_view, @@ -599,14 +604,16 @@ fn dispatch_button_nodes(view: WorkRowView) -> List { // route records the Claimed event and then drives the SAME launch admission the /dispatch route // answers (see gunbc.roadmap_serve serve_issue_assign_response_over), refusing with the launch's // typed vocabulary when fabric cannot start. -fn issue_assign_action_nodes(view: WorkRowView, viewer: String, recents: List, expected_revision: String) -> List { +fn issue_assign_action_nodes(view: WorkRowView, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> List { let suggestions = issue_assign_suggestions(viewer: viewer, recents: recents) [ el_attrs("form", [ attr(name: "class", value: "issue-assign-form" ), attr(name: "method", value: "post" ), - attr(name: "enctype", value: "text/plain" ), + attr(name: "enctype", value: "application/x-www-form-urlencoded" ), attr(name: "action", value: join(["/issue/", view.node_id, "/assign"], "") ), ], [ + el_attrs("input", [ attr(name: "name", value: "key"), attr(name: "type", value: "hidden"), attr(name: "value", value: if command_context.operation == "" { "" } else { concat(command_context.operation, "-assign") }) ], []), + el_attrs("input", [ attr(name: "name", value: "csrf"), attr(name: "type", value: "hidden"), attr(name: "value", value: command_context.csrf) ], []), el_attrs("input", [ attr(name: "class", value: "issue-assign-input" ), attr(name: "name", value: "assignee" ), @@ -628,7 +635,6 @@ fn issue_assign_action_nodes(view: WorkRowView, viewer: String, recents: List Fragment { // THE DETAIL ACTION BAR: the durable Assign form alone (owner ruling 2026-09-25 — the table rows // and the detail header carry no execution buttons; see issue_assign_action_nodes for the ruling). -fn issue_detail_action_bar_nodes(view: WorkRowView, viewer: String, recents: List, expected_revision: String) -> List { - issue_assign_action_nodes(view: view, viewer: viewer, recents: recents, expected_revision: expected_revision) +fn issue_detail_action_bar_nodes(view: WorkRowView, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> List { + issue_assign_action_nodes(view: view, viewer: viewer, recents: recents, expected_revision: expected_revision, command_context: command_context) } data workflow_controls_actions_only_note: String = "The actuator cell carries ACTIONS ONLY (A2, the falsifier correction — see roadmap_row_archetype_note). The server-rendered lamp strip, seven pending stage chips, and 'waiting' activity text that used to preload this wrapper were exactly the furniture the attention rule forbids: a row with no attempt renders NO workflow DOM at all, and a row with one gets its activity painted by the client into the under-head ActivityRegion from the DECIDED /workflow.json view — never into this cell. The wrapper stays always-present so the dispatch control keeps a stable actuator home and the head grid keeps its three derived tracks." @@ -947,7 +953,7 @@ fn taskbar_fill_nodes(view: WorkRowView, bar: TaskBarView, project: ProjectTreeV taskbar_stage_fill(obligations: obligations, live: view.worker_alive) NoActiveWork => [] LegacySupersedableAttempt { summary: _, located_reason: _, attempt_key: _, evidence: _ } => [] - ActivityUnobservable { reason: _ } => [] + ActivityUnobservable { reason } => [ el_class("p", "issue-observation-unavailable", [ txt(concat("Work observation unavailable: ", reason)) ]) ] } } } @@ -985,7 +991,7 @@ fn taskbar_attention_attr(view: WorkRowView) -> List { LegacySupersedableAttempt { summary: _, located_reason: _, attempt_key: _, evidence: _ } => [] CompletedWork { outcome: _, evidence: _, obligations: _ } => [] NoActiveWork => [] - ActivityUnobservable { reason: _ } => [] + ActivityUnobservable { reason } => [ el_class("p", "issue-observation-unavailable", [ txt(concat("Work observation unavailable: ", reason)) ]) ] } } @@ -1029,7 +1035,7 @@ fn inspect_attempt_nodes(view: WorkRowView, obligations: List, fn inspect_attempt_evidence(view: WorkRowView) -> List { match view.activity { NoActiveWork => [] - ActivityUnobservable { reason: _ } => [] + ActivityUnobservable { reason } => [ el_class("p", "issue-observation-unavailable", [ txt(concat("Work observation unavailable: ", reason)) ]) ] ActiveWork { summary: _, obligations, remaining_obligation_count: _, current_activity: _, attention: _, evidence } => inspect_attempt_nodes(view: view, obligations: obligations, evidence: evidence, located_reason: "") ActiveWorkRefused { summary: _, located_reason, evidence, obligations } => @@ -1536,38 +1542,10 @@ fn issue_parent_value(panel: IssueLinkPanel) -> List { } } -// THE ASSIGNEE IS THE WORK'S HOLDER: when an attempt exists, the assignee surface shows the -// attempt's session identity (its short key — who holds the work now); with no attempt, the -// event-log standing's holder (who claimed the node); open / forked / incomplete standings name -// no holder — the field falls back to the model's unassigned placeholder, never the owner -// relabeled. The empty string this fn returns for "no holder" is the caller's signal to render -// the placeholder (issue_assignee_unassigned_display, "--" per the capture) — the Buganizer -// results row never leaves the cell blank. Changing the assignee is the follow-on intake slice; -// this is the decided identity read as a field. -// THE HOLDER, the identity the assignee surfaces render: when a live attempt holds the work, the -// WORKFLOW principal — a session or attempt key is not an identity and never surfaces as one -// (owner 2026-09-26: "get off the legacy session thing"; the identity ruling's PrincipalRef law; -// the attempt evidence itself lives in the Work tab) — else the standing's holder principal. An -// IDENTITY, never a bare string, so the chip and the rail render through the display projection. -// Absent names no holder. +// Assignment is the committed command projection. Attempt existence and progress actors do not +// grant ownership; table and detail read the same holder from the same causal state. fn issue_assignee_holder(view: WorkRowView, attempts: List) -> EventPrincipal? { - match attempts.first() { - Present { value: _ } => Present { value: EventPrincipalAuthenticated { ref: fabric_workflow_principal } } - Absent => - match view.standing { - Absent => none - Present { value: s } => match s { - NodeClaimed { by, since: _ } => Present { value: by } - NodeInProgress { by, last_note: _ } => Present { value: by } - NodeBlocked { by, reason: _ } => Present { value: by } - NodeHandedBack { by, reason: _ } => Present { value: by } - NodeReadyForReview { by, pr: _, head: _ } => Present { value: by } - NodeOpen => none - NodeForked { heads: _ } => none - NodeHistoryIncomplete { missing_parents: _ } => none - } - } - } + match view.assignment { Present { value: assignment } => assignment.holder Absent => none } } // THE FIELD-ROW TEXT for the assignee row (the rail's plain-text spelling): the holder's display @@ -1681,7 +1659,7 @@ fn issue_line_sort_value(col: IssueResultsColumn, view: WorkRowView, bar: TaskBa } } -fn issue_line_li(row: IssueTableRow, observation: ObservationImpactView, edges: List) -> Fragment { +fn issue_line_li(row: IssueTableRow, observation: ObservationImpactView, edges: List, memberships: List = declared_roadmap_memberships()) -> Fragment { let view = row.view let bar = if view.attempt_stale { task_bar_idle_view(view: view) } else { task_bar_view_of(view: view, observation: observation) } Element { @@ -1689,6 +1667,8 @@ fn issue_line_li(row: IssueTableRow, observation: ObservationImpactView, edges: attributes: concat( node_li_attributes_for(view: view, class_infix: "") |> filter(a => !(a.name == "id")), [ attr(name: "role", value: "row"), + attr(name: "data-project", value: match project_root_for_node(node_id: view.node_id, memberships: memberships) { Present { value: project } => project Absent => "" }), + attr(name: "data-complete", value: if work_lifecycle_key(l: view.lifecycle) == "done" { "true" } else { "false" }), attr(name: "data-dependencies", value: join(map(filter(edges, e => e.child as String == view.node_id), e => e.parent as String), " ") ), attr(name: "data-search", value: issue_search_value(view: view) ), attr(name: "data-summary-only", value: "" ) ]), @@ -1723,12 +1703,12 @@ fn issue_table_header_nodes() -> Fragment { ], [ txt(c.header as String) ]) ]))) } -fn issue_table_markup(rows: List, observation: ObservationImpactView, edges: List) -> Fragment { +fn issue_table_markup(rows: List, observation: ObservationImpactView, edges: List, memberships: List = declared_roadmap_memberships()) -> Fragment { el_attrs("div", [attr(name: "class", value: "issue-table"), attr(name: "role", value: "table"), attr(name: "aria-label", value: "Issue search results")], [ el_class("h2", "issue-table-heading", [ txt("Issue search results") ]), issue_table_header_nodes(), el_class("div", "issue-empty", [ txt("No issues match your search.") ]), - el_attrs("ul", [attr(name: "role", value: "rowgroup")], map(rows, r => issue_line_li(row: r, observation: observation, edges: edges))), + el_attrs("ul", [attr(name: "role", value: "rowgroup")], map(rows, r => issue_line_li(row: r, observation: observation, edges: edges, memberships: memberships))), ]) } @@ -1844,22 +1824,9 @@ fn issue_event_text(env: RoadmapEventEnvelope) -> String { issue_event_payload(env: env) } -// Insertion sort by recorded_at, descending — event histories are small (a node's durable facts), -// and the sort keeps the newest turn at the head without a general ordering substrate. +// The audit and last-modified projection share causal order, independent of clock skew. fn issue_events_newest_first(envs: List) -> List { - fold(envs, init: [], f: (acc, env) => issue_event_insert_sorted(sorted: acc, env: env)) -} - -fn issue_event_insert_sorted(sorted: List, env: RoadmapEventEnvelope) -> List { - match sorted.first() { - Absent => [ env ] - Present { value: head } => - if head.event.recorded_at <= env.event.recorded_at { - concat([env], sorted) - } else { - concat([head], issue_event_insert_sorted(sorted: sorted.skip(n: 1), env: env)) - } - } + roadmap_issue_history_newest_first(envs: envs) } @@ -1892,10 +1859,10 @@ fn issue_authored_section(heading: String, rows: List) -> Fragment { rows)) } -// THE FIELDS OF ONE LABEL, in the label order the caller names — the fold groups by the -// interface's own field labels, never re-derives their content. -fn ticket_fields_labeled(fields: List, labels: List) -> List { - fold(labels, init: [], f: (acc, label) => concat(acc, filter(fields, f => f.label == label))) +// Typed field identities survive presentation. Display labels may change without moving a +// field into a different semantic section. +fn ticket_fields_selected(fields: List, identities: List) -> List { + fold(identities, init: [], f: (acc, identity) => concat(acc, filter(fields, f => f.identity == identity))) } // THE AUTHORED FIELDS, FOLDED PER THE MANDATE (owner 2026-09-25, @@ -1912,34 +1879,29 @@ fn disclosure_folded_nodes(d: DisclosureView, done: Bool) -> List { LegacyBodyDisclosure { content } => markdown_inlines_to_fragments(inlines: parse_inline_markdown_minimal(text: content)) TicketDisclosure { brief, fields, updates } => - let description = issue_authored_section(heading: "Description", rows: [ el_class("div", "ticket-brief", inline_md_nodes(text: brief)) ]) - let acceptance_fields = ticket_fields_labeled(fields: fields, labels: ["red control", "hand-back"]) + let acceptance_fields = ticket_fields_selected(fields: fields, identities: [TicketRedControl, TicketHandback]) let acceptance = if count(acceptance_fields) == 0 { [] } else { [ issue_authored_section(heading: "Acceptance criteria", rows: map(acceptance_fields, f => disclosure_field_row(f: f))) ] } - let plan_fields = ticket_fields_labeled(fields: fields, labels: ["current state", "first slice", "displaced cost"]) + let plan_fields = ticket_fields_selected(fields: fields, identities: [TicketCurrentState, TicketFirstSlice, TicketDisplacedCost]) let plan = if count(plan_fields) == 0 { [] } else { [ issue_authored_details(summary: "Current plan", open: !done, rows: map(plan_fields, f => disclosure_field_row(f: f))) ] } - let scope_fields = ticket_fields_labeled(fields: fields, labels: ["out of scope"]) + let scope_fields = ticket_fields_selected(fields: fields, identities: [TicketOutOfScope]) let scope = if count(scope_fields) == 0 { [] } else { [ issue_authored_details(summary: "Scope", open: false, rows: map(scope_fields, f => disclosure_field_row(f: f))) ] } [ el_class("div", "issue-authored", concat( - [ description ], + [], concat(acceptance, concat(plan, concat(scope, disclosure_updates_block(updates: updates)))))) ] } } -// THE DESCRIPTION, pinned above the comment stream (owner mandate 2026-09-25, -// docs/plans/issue-page-architecture.md): the issue's authored content leads the page's narrative. -// THE NAMED GAP, not papered over: the roadmap authority rows carry one content string today (the -// headline the header also wears), so the description renders THAT content until the authority -// grows a separate body field — never a fabricated empty section. +// The authored brief is the description; the headline remains the issue title. fn issue_description_node(view: WorkRowView) -> Fragment { el_class("div", "issue-description", [ el_class("h2", "issue-description-heading", [ txt("Description") ]), - el_class("div", "issue-description-body", inline_md_nodes(text: view.headline)), + el_class("div", "issue-description-body", inline_md_nodes(text: match view.disclosure { TicketDisclosure { brief, fields: _, updates: _ } => brief LegacyBodyDisclosure { content } => content NoDisclosure => view.headline })), ]) } @@ -2107,15 +2069,15 @@ fn issue_publication_merge_target(envs: List) -> String { // (gunbc.auth.request_authentication.human_write_admission), and the expected_revision pin // matching the assign form. The submit wears no .issue-action class — the client's bodyless action // bind must not race the native submit. -fn issue_comment_composer_node(node_id: String, expected_revision: String) -> Fragment { +fn issue_comment_composer_node(node_id: String, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> Fragment { el_attrs("form", [ attr(name: "class", value: "issue-comment-form" ), attr(name: "method", value: "post" ), - attr(name: "enctype", value: "text/plain" ), + attr(name: "enctype", value: "application/x-www-form-urlencoded" ), attr(name: "action", value: join(["/issue/", node_id, "/comment"], "") ), ], [ - el_attrs("input", [ attr(name: "name", value: "key" ), attr(name: "type", value: "hidden" ), attr(name: "value", value: "" ) ], []), - el_attrs("input", [ attr(name: "name", value: "csrf" ), attr(name: "type", value: "hidden" ), attr(name: "value", value: "" ) ], []), + el_attrs("input", [ attr(name: "name", value: "key" ), attr(name: "type", value: "hidden" ), attr(name: "value", value: if command_context.operation == "" { "" } else { concat(command_context.operation, "-comment") } ) ], []), + el_attrs("input", [ attr(name: "name", value: "csrf" ), attr(name: "type", value: "hidden" ), attr(name: "value", value: command_context.csrf ) ], []), el_attrs("input", [ attr(name: "name", value: "expected_revision" ), attr(name: "type", value: "hidden" ), attr(name: "value", value: expected_revision ) ], []), el_attrs("textarea", [ attr(name: "name", value: "body" ), @@ -2143,7 +2105,7 @@ fn issue_work_tab_nodes(view: WorkRowView, attempts: List [] - ActivityUnobservable { reason: _ } => [] + ActivityUnobservable { reason } => [ el_class("p", "issue-observation-unavailable", [ txt(concat("Work observation unavailable: ", reason)) ]) ] ActiveWork { summary: _, obligations, remaining_obligation_count: _, current_activity: _, attention: _, evidence } => issue_work_ledger_nodes(view: view, obligations: obligations, evidence: evidence, located_reason: "") ActiveWorkRefused { summary: _, located_reason, evidence, obligations } => @@ -2198,14 +2160,10 @@ fn issue_tab_chip(label: String, tab: String) -> Fragment { // does not yet — the chip omits, never fabricates). The standing line, the live bar signal and // the assignee control moved to the metadata rail. fn issue_head_chip_nodes(view: WorkRowView) -> List { - concat( - status_chip_nodes(view: view), - concat( - [ el_class("span", "issue-type-chip", [ txt("Task") ]) ], - [])) + status_chip_nodes(view: view) } -fn issue_detail_page(view: WorkRowView, panel: IssueLinkPanel, observation: ObservationImpactView, attempts: List, events: List, viewer: String, recents: List, expected_revision: String) -> Fragment { +fn issue_detail_page(view: WorkRowView, panel: IssueLinkPanel, observation: ObservationImpactView, attempts: List, events: List, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> Fragment { el_attrs("main", [ attr(name: "class", value: "issue-detail" ), attr(name: "data-tab", value: "comments" ), @@ -2223,35 +2181,29 @@ fn issue_detail_page(view: WorkRowView, panel: IssueLinkPanel, observation: Obse ]), el_class("div", "issue-body", [ el_class("div", "issue-panels", [ - el_class("section", "panel-comments", issue_comments_tab_nodes(view: view, events: events, expected_revision: expected_revision)), + el_class("section", "panel-comments", issue_comments_tab_nodes(view: view, events: events, expected_revision: expected_revision, command_context: command_context)), el_class("section", "panel-dependencies", issue_dependencies_tab_nodes(view: view, panel: panel)), el_class("section", "panel-work", issue_work_tab_nodes(view: view, attempts: attempts, events: events)), el_class("section", "panel-history", issue_history_tab_nodes(events: events)), ]), - issue_metadata_rail_node(view: view, panel: panel, attempts: attempts, events: events, viewer: viewer, recents: recents, expected_revision: expected_revision), + issue_metadata_rail_node(view: view, panel: panel, attempts: attempts, events: events, viewer: viewer, recents: recents, expected_revision: expected_revision, command_context: command_context), ]), ]) } // THE COMMENTS TAB: the description pinned above the stream, the composer below — the page's // default panel (the tab law renders it when data-tab is "comments"). -fn issue_comments_tab_nodes(view: WorkRowView, events: List, expected_revision: String) -> List { +fn issue_comments_tab_nodes(view: WorkRowView, events: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> List { concat( [ issue_description_node(view: view), issue_comments_stream_node(node_id: view.node_id, envs: events), - issue_comment_composer_node(node_id: view.node_id, expected_revision: expected_revision), + issue_comment_composer_node(node_id: view.node_id, expected_revision: expected_revision, command_context: command_context), ], disclosure_folded_nodes(d: view.disclosure, done: work_lifecycle_key(l: view.lifecycle) == "done")) } -// THE DEPENDENCIES TAB, dimensional per the owner mandate: parent/children (nesting), blocked -// by/blocking (must-complete-first), related, and supersedes — separate sections, never one -// flattened list. THE NAMED GAP, stated not papered over: the roadmap graph carries only -// dependency edges today, so BOTH nesting sides ride the primary-parent rule over those edges -// (the capture-verified decomposition approximation from the qualified-identity ruling) — the -// section labels keep the dimensions distinct for when decomposition edges land, and "related" -// is declared with no relation kind yet (the section renders its emptiness honestly). +// Nesting and blocking are independently projected; no reverse blocker stands in for a child. fn issue_dependencies_tab_nodes(view: WorkRowView, panel: IssueLinkPanel) -> List { [ el_class("div", "issue-deps-section", [ @@ -2260,7 +2212,7 @@ fn issue_dependencies_tab_nodes(view: WorkRowView, panel: IssueLinkPanel) -> Lis ]), el_class("div", "issue-deps-section", [ el_class("h3", "issue-deps-heading", [ txt("children") ]), - el_class("div", "issue-deps-body", if count(panel.blocking) == 0 { [ txt("none") ] } else { issue_link_value(links: panel.blocking) }), + el_class("div", "issue-deps-body", if count(panel.children) == 0 { [ txt("no nesting declared") ] } else { issue_link_value(links: panel.children) }), ]), el_class("div", "issue-deps-section", [ el_class("h3", "issue-deps-heading", [ txt("blocked by") ]), @@ -2268,7 +2220,7 @@ fn issue_dependencies_tab_nodes(view: WorkRowView, panel: IssueLinkPanel) -> Lis ]), el_class("div", "issue-deps-section", [ el_class("h3", "issue-deps-heading", [ txt("blocking") ]), - el_class("div", "issue-deps-body", [ txt("rides the same primary-parent rule until dependency-reverse edges are declared — named, not fabricated") ]), + el_class("div", "issue-deps-body", if count(panel.blocking) == 0 { [ txt("none") ] } else { issue_link_value(links: panel.blocking) }), ]), el_class("div", "issue-deps-section", [ el_class("h3", "issue-deps-heading", [ txt("related") ]), @@ -2294,12 +2246,12 @@ fn issue_history_tab_nodes(events: List) -> List // component, parent, blocked-by/blocking, delivery, return-to, modified, observation — the // issue's identity and state at a glance, beside the narrative. Rows with no fact OMIT (the // mandate's empty-row law), except status and assignee which always render. -fn issue_metadata_rail_node(view: WorkRowView, panel: IssueLinkPanel, attempts: List, events: List, viewer: String, recents: List, expected_revision: String) -> Fragment { +fn issue_metadata_rail_node(view: WorkRowView, panel: IssueLinkPanel, attempts: List, events: List, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> Fragment { let assignment = roadmap_issue_assignment(envs: events) let bar = task_bar_view_of(view: view, observation: observation_impact_view(workflow_refused: none)) el_class("aside", "issue-rail", [ issue_field_text_row(label: "status", value: issue_rail_status_text(view: view, events: events)), - issue_rail_assignee_row(view: view, attempts: attempts, assignment: assignment, viewer: viewer, recents: recents, expected_revision: expected_revision), + issue_rail_assignee_row(view: view, attempts: attempts, assignment: assignment, viewer: viewer, recents: recents, expected_revision: expected_revision, command_context: command_context), issue_field_text_row(label: "reporter", value: issue_owner_value(view: view)), issue_field_text_row(label: "component", value: issue_component_value(view: view)), issue_field_row(label: "parent", value: issue_parent_value(panel: panel)), @@ -2327,14 +2279,14 @@ fn issue_rail_status_text(view: WorkRowView, events: List) // THE ASSIGNEE ROW: the current holder's chip (or the unassigned placeholder) beside the durable // Assign control — the editable field the mandate puts in the rail. -fn issue_rail_assignee_row(view: WorkRowView, attempts: List, assignment: IssueAssignmentProjection, viewer: String, recents: List, expected_revision: String) -> Fragment { - let holder_chip = match issue_assignee_holder(view: view, attempts: attempts) { +fn issue_rail_assignee_row(view: WorkRowView, attempts: List, assignment: IssueAssignmentProjection, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> Fragment { + let holder_chip = match assignment.holder { Absent => [ txt(issue_assignee_unassigned_display as String) ] Present { value: holder } => [ issue_assignee_chip_node(holder: holder) ] } issue_field_row(label: "assignee", value: concat( holder_chip, - issue_detail_action_bar_nodes(view: view, viewer: viewer, recents: recents, expected_revision: expected_revision))) + issue_detail_action_bar_nodes(view: view, viewer: viewer, recents: recents, expected_revision: expected_revision, command_context: command_context))) } // THE RETURN-TO ROW: the stored merge owner the work comes back to (the publication handoff's @@ -2380,7 +2332,7 @@ fn issue_modified_value(events: List) -> String { } } -fn issue_detail_page_launch(view: WorkRowView, panel: IssueLinkPanel, observation: ObservationImpactView, attempts: List, events: List, viewer: String, recents: List, expected_revision: String) -> Fragment { +fn issue_detail_page_launch(view: WorkRowView, panel: IssueLinkPanel, observation: ObservationImpactView, attempts: List, events: List, viewer: String, recents: List, expected_revision: String, command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }) -> Fragment { Element { tag: "html", attributes: [ attr(name: "lang", value: "en" ) ], @@ -2393,7 +2345,7 @@ fn issue_detail_page_launch(view: WorkRowView, panel: IssueLinkPanel, observatio ]), el("body", [ roadmap_header(page: "issue"), - issue_detail_page(view: view, panel: panel, observation: observation, attempts: attempts, events: events, viewer: viewer, recents: recents, expected_revision: expected_revision), + issue_detail_page(view: view, panel: panel, observation: observation, attempts: attempts, events: events, viewer: viewer, recents: recents, expected_revision: expected_revision, command_context: command_context), dispatch_client_script(), ]), ], @@ -2437,12 +2389,13 @@ fn issue_detail_page_for( events: List, viewer: String, recents: List, -) -> Fragment { + command_context: IssueFormContext = IssueFormContext { operation: "", csrf: "" }, + memberships: List = declared_roadmap_memberships()) -> Fragment { match proj { RoadmapAuthorityProjectionRefused { detail: detail } => roadmap_daily_workspace_page_for_projection_observed_stale( proj: proj, attempts: attempts, attempts_refused: attempts_refused, - standings: standings, launch: launch, stale_nodes: stale_nodes) + standings: standings, launch: launch, stale_nodes: stale_nodes, memberships: memberships) RoadmapAuthorityProjected { doc, accepted: accepted, withheld: withheld } => let nodes = doc_all_nodes(doc: doc) let edges = doc_all_edges(doc: doc) @@ -2459,15 +2412,16 @@ fn issue_detail_page_for( rounds: daily_row_rounds(node_id: node_id, attempts: attempts, attempts_refused: attempts_refused), worker_alive: daily_row_worker_alive(node_id: node_id, attempts: attempts, attempts_refused: attempts_refused), standing: standing_for_node(standings: standings, node_id: node_id), + assignment: assignment_for_node(standings: standings, node_id: node_id), launch: ctx, attempt_stale: any(stale_nodes, s => s == node_id)), - panel: gunbc.roadmap_presentation.issue_link_panel(rn: rn, nodes: nodes, edges: edges), + panel: gunbc.roadmap_presentation.issue_link_panel(rn: rn, nodes: nodes, edges: edges, memberships: memberships), observation: observation, attempts: attempts, events: events, viewer: viewer, recents: recents, - expected_revision: issue_chain_head_id(envs: events)) + expected_revision: issue_chain_head_id(envs: events), command_context: command_context) } } } @@ -2500,11 +2454,11 @@ fn issue_detail_not_found_page(node_id: String, viewer: String) -> Fragment { // vocabulary outlives the band SURFACE. The forest renderer below (workspace_entries_ul and its // chain) stays in this module as the /sandbox exhibit's real grouping path; nothing on this page // calls it. -fn workspace_markup(ws: DailyWorkspaceView, edges: List) -> Fragment { +fn workspace_markup(ws: DailyWorkspaceView, edges: List, memberships: List = declared_roadmap_memberships()) -> Fragment { el_class("section", "roadmap-frontier", [ el_class("div", "view-table", concat( [ issue_controls_markup() ], - [ issue_table_markup(rows: ws.table, observation: ws.observation, edges: edges) ])), + [ issue_table_markup(rows: ws.table, observation: ws.observation, edges: edges, memberships: memberships) ])), ]) } @@ -2605,7 +2559,7 @@ fn daily_workspace_view_of_for_accepted( standings: RoadmapStandings, launch: LaunchHostStanding, stale_nodes: List, -) -> DailyWorkspaceView { + memberships: List = declared_roadmap_memberships()) -> DailyWorkspaceView { let nodes = doc_all_nodes(doc: doc) let edges = doc_all_edges(doc: doc) let observation = observation_impact_view(workflow_refused: attempts_refused) @@ -2616,6 +2570,7 @@ fn daily_workspace_view_of_for_accepted( rounds: daily_row_rounds(node_id: rn.node as String, attempts: attempts, attempts_refused: attempts_refused), worker_alive: daily_row_worker_alive(node_id: rn.node as String, attempts: attempts, attempts_refused: attempts_refused), standing: standing_for_node(standings: standings, node_id: rn.node as String), + assignment: assignment_for_node(standings: standings, node_id: rn.node as String), launch: ctx, attempt_stale: any(stale_nodes, s => s == rn.node as String))) let ready_count = count(rows |> filter(r => @@ -2626,7 +2581,7 @@ fn daily_workspace_view_of_for_accepted( program: Present { value: v1_exit_program_progress_view_for_accepted(accepted_ids: accepted_ids, ready_count: ready_count) }, observation: observation, families: live_work_family_decls(), - members: live_work_family_members_for_accepted(accepted: accepted_ids, withheld: withheld)) + members: live_work_family_members_for_accepted(accepted: accepted_ids, withheld: withheld), memberships: memberships) } fn roadmap_daily_workspace_page_impl_for_accepted( @@ -2640,7 +2595,7 @@ fn roadmap_daily_workspace_page_impl_for_accepted( standings: RoadmapStandings, launch: LaunchHostStanding, stale_nodes: List, -) -> Fragment { + memberships: List = declared_roadmap_memberships()) -> Fragment { let ws = daily_workspace_view_of_for_accepted( doc: doc, merged: merged, @@ -2650,7 +2605,7 @@ fn roadmap_daily_workspace_page_impl_for_accepted( attempts_refused: attempts_refused, standings: standings, launch: launch, stale_nodes: stale_nodes, - ) + memberships: memberships) let program_strip = match ws.program { Present { value: v } => [ program_progress_markup_of(v: v) ] Absent => [] @@ -2669,7 +2624,7 @@ fn roadmap_daily_workspace_page_impl_for_accepted( roadmap_header(page: "daily workspace"), el_class("main", "daily-workspace", concat(main_prefix, concat(program_strip, concat( workspace_observation_banner(observation: ws.observation), - [ workspace_markup(ws: ws, edges: doc_all_edges(doc: doc))])))), + [ workspace_markup(ws: ws, edges: doc_all_edges(doc: doc), memberships: memberships)])))), dispatch_client_script(), ]), ], @@ -2830,12 +2785,12 @@ fn roadmap_page_for_projection_launch(proj: RoadmapAuthorityProjection, launch: // can name the public accepted set any more, and these two get the empty one, which is what their // own "the event carrier is not consulted" already claimed and did not deliver. -fn roadmap_daily_workspace_page(doc: RoadmapDocument, merged: List) -> Fragment { - roadmap_daily_workspace_page_impl_for_accepted(doc: doc, merged: merged, accepted_ids: [], withheld: [], main_prefix: [], attempts: [], attempts_refused: none, standings: StandingsUnobserved { reason: "static render: the event carrier is not consulted" }, launch: launch_host_unobserved_static_render(), stale_nodes: []) +fn roadmap_daily_workspace_page(doc: RoadmapDocument, merged: List, memberships: List = declared_roadmap_memberships()) -> Fragment { + roadmap_daily_workspace_page_impl_for_accepted(doc: doc, merged: merged, accepted_ids: [], withheld: [], main_prefix: [], attempts: [], attempts_refused: none, standings: StandingsUnobserved { reason: "static render: the event carrier is not consulted" }, launch: launch_host_unobserved_static_render(), stale_nodes: [], memberships: memberships) } -fn roadmap_daily_workspace_page_launch(doc: RoadmapDocument, merged: List, launch: LaunchHostStanding) -> Fragment { - roadmap_daily_workspace_page_impl_for_accepted(doc: doc, merged: merged, accepted_ids: [], withheld: [], main_prefix: [], attempts: [], attempts_refused: none, standings: StandingsUnobserved { reason: "static render: the event carrier is not consulted" }, launch: launch, stale_nodes: []) +fn roadmap_daily_workspace_page_launch(doc: RoadmapDocument, merged: List, launch: LaunchHostStanding, memberships: List = declared_roadmap_memberships()) -> Fragment { + roadmap_daily_workspace_page_impl_for_accepted(doc: doc, merged: merged, accepted_ids: [], withheld: [], main_prefix: [], attempts: [], attempts_refused: none, standings: StandingsUnobserved { reason: "static render: the event carrier is not consulted" }, launch: launch, stale_nodes: [], memberships: memberships) } // The served daily workspace consumes the OBSERVED attempt evidence at render (the tick decomposes the belt observation and passes progress rows or the located refusal), so the active band and every server-rendered activity region are real facts, not placeholders — the same decided views the /workflow.json wire carries, giving the client poll an identical starting DOM (the parity obligation's server half). The plan is a parameter because the caller that renders it is the caller that chose it. @@ -2845,7 +2800,7 @@ fn roadmap_daily_workspace_page_for_projection_observed( attempts_refused: String?, standings: RoadmapStandings, launch: LaunchHostStanding, -) -> Fragment { + memberships: List = declared_roadmap_memberships()) -> Fragment { roadmap_daily_workspace_page_for_projection_observed_stale( proj: proj, attempts: attempts, @@ -2853,7 +2808,7 @@ fn roadmap_daily_workspace_page_for_projection_observed( standings: standings, launch: launch, stale_nodes: [], - ) + memberships: memberships) } // THE STALE-AWARE ENTRY: the tick's observing authority (gunbc.roadmap_served_observation) reads @@ -2870,7 +2825,7 @@ fn roadmap_daily_workspace_page_for_projection_observed_stale( standings: RoadmapStandings, launch: LaunchHostStanding, stale_nodes: List, -) -> Fragment { + memberships: List = declared_roadmap_memberships()) -> Fragment { match proj { RoadmapAuthorityProjectionRefused { detail: detail } => roadmap_daily_workspace_page_impl_for_accepted( @@ -2887,7 +2842,7 @@ fn roadmap_daily_workspace_page_for_projection_observed_stale( standings: standings, launch: launch, stale_nodes: stale_nodes, - ) + memberships: memberships) RoadmapAuthorityProjected { doc: doc, accepted: accepted, withheld: withheld } => roadmap_daily_workspace_page_impl_for_accepted( doc: doc, @@ -2902,20 +2857,20 @@ fn roadmap_daily_workspace_page_for_projection_observed_stale( standings: standings, launch: launch, stale_nodes: stale_nodes, - ) + memberships: memberships) } } -fn roadmap_daily_workspace_page_for_authority() -> Fragment { - roadmap_daily_workspace_page_for_projection(proj: roadmap_authority_projection()) +fn roadmap_daily_workspace_page_for_authority( memberships: List = declared_roadmap_memberships()) -> Fragment { + roadmap_daily_workspace_page_for_projection(proj: roadmap_authority_projection(), memberships: memberships) } -fn roadmap_daily_workspace_page_for_projection(proj: RoadmapAuthorityProjection) -> Fragment { +fn roadmap_daily_workspace_page_for_projection(proj: RoadmapAuthorityProjection, memberships: List = declared_roadmap_memberships()) -> Fragment { roadmap_daily_workspace_page_for_projection_observed( proj: proj, attempts: [], attempts_refused: none, standings: StandingsUnobserved { reason: "static render: the event carrier is not consulted" }, launch: launch_host_unobserved_static_render(), - ) + memberships: memberships) } diff --git a/dag/gunbc/roadmap/roadmap_presentation.dag b/dag/gunbc/roadmap/roadmap_presentation.dag index 8525e02b2dc..99f87e4d0cc 100644 --- a/dag/gunbc/roadmap/roadmap_presentation.dag +++ b/dag/gunbc/roadmap/roadmap_presentation.dag @@ -1,7 +1,10 @@ module gunbc.roadmap_presentation +import gunbc.roadmap_nesting { RoadmapMembership, declared_roadmap_memberships } +import gunbc.roadmap.roadmap_project { node_project_membership, MembershipMember, project_member_ids } + import gunbc.roadmap.roadmap_event_log { - RoadmapNodeStanding, + RoadmapNodeStanding, IssueAssignmentProjection, NodeOpen, NodeClaimed, NodeInProgress, NodeBlocked, NodeHandedBack, NodeForked, NodeHistoryIncomplete, } import gunbc.roadmap_model { @@ -30,7 +33,7 @@ import gunbc.roadmap_provider_events { provider_execution_activity, ProviderEventsRefused, } -import extdeps.google.issue_tracker { fleet_component_hosts } +import gunbc.roadmap_fleet_components { spark_host_component_paths } import gunbc.roadmap_workflow_stage { WorkflowSegmentKind, WorkflowSegment, WorkflowSegmentState, WorkflowSegmentPending, WorkflowSegmentActive, WorkflowSegmentComplete, @@ -368,7 +371,17 @@ type SupportingFact { // row with neither has NoDisclosure and renders no details element beyond its supporting facts. // Markdown-inline serialization of these strings is the renderer's job — serialization, not // decision. +type TicketFieldIdentity + = TicketDisplacedCost + | TicketCurrentState + | TicketFirstSlice + | TicketRedControl + | TicketOutOfScope + | TicketHandback + | TicketVerified + type DisclosureField { + identity: TicketFieldIdentity label: String value: String } @@ -401,6 +414,7 @@ type WorkRowView { rounds: HarnessRoundLedger worker_alive: Bool standing: RoadmapNodeStanding? + assignment: IssueAssignmentProjection? attempt_stale: Bool } @@ -467,16 +481,7 @@ fn carrier_facts(carriers: List) -> List { map(carriers, p => SupportingFact { label: "carrier", value: p.label, href: p.path }) } -// THE COMPONENT PLACEMENT (owner ruling 2026-09-24: fleet issues carry a real component -// placement, the srvN tree in extdeps.google.issue_tracker): a carrier under dag/gunbc/spark/ is -// the Spark lane's work, placed on the Spark hosts — derived from the component tree, never a -// hand-listed string. Finer arm placement (group A vs B) is authored content knowledge, not -// carrier-derivable; the browse tree carries the arms, the issue's field names the hosts its lane -// touches. -fn spark_host_component_paths() -> List { - map(filter(fleet_component_hosts, c => c.id >= 5), c => join(c.path, "/")) -} - +// Fleet component paths are projected by the fleet-owned adapter. fn component_fact(carriers: List) -> List { if any(carriers, p => starts_with(s: p.path, prefix: "dag/gunbc/spark/")) { [ SupportingFact { label: "component", value: join(spark_host_component_paths(), " · "), href: "" } ] @@ -530,8 +535,8 @@ fn row_summary(line: RoadmapItem) -> String? { none } -fn disclosure_field(label: String, value: String) -> List { - if value == "" { [] } else { [ DisclosureField { label: label, value: value } ] } +fn disclosure_field(identity: TicketFieldIdentity, label: String, value: String) -> List { + if value == "" { [] } else { [ DisclosureField { identity: identity, label: label, value: value } ] } } // THE VERIFIED ROW'S LAW (owner mandate 2026-09-25): an empty `verified · last verified` row @@ -542,17 +547,17 @@ fn verified_disclosure_field(t: TicketFields) -> List { if t.last_verified_on == "" { [] } else { - disclosure_field(label: "verified", value: join([t.authored_on, " · last verified ", t.last_verified_on], "")) + disclosure_field(identity: TicketVerified, label: "verified", value: join([t.authored_on, " · last verified ", t.last_verified_on], "")) } } fn ticket_disclosure_fields(t: TicketFields) -> List { - concat(disclosure_field(label: "displaced cost", value: t.displaced_cost), - concat(disclosure_field(label: "current state", value: t.current_state), - concat(disclosure_field(label: "first slice", value: t.first_slice), - concat(disclosure_field(label: "red control", value: t.red_control), - concat(disclosure_field(label: "out of scope", value: t.out_of_scope), - concat(disclosure_field(label: "hand-back", value: t.handback), + concat(disclosure_field(identity: TicketDisplacedCost, label: "displaced cost", value: t.displaced_cost), + concat(disclosure_field(identity: TicketCurrentState, label: "current state", value: t.current_state), + concat(disclosure_field(identity: TicketFirstSlice, label: "first slice", value: t.first_slice), + concat(disclosure_field(identity: TicketRedControl, label: "red control", value: t.red_control), + concat(disclosure_field(identity: TicketOutOfScope, label: "out of scope", value: t.out_of_scope), + concat(disclosure_field(identity: TicketHandback, label: "hand-back", value: t.handback), verified_disclosure_field(t: t))))))) } @@ -1022,6 +1027,7 @@ fn work_row_view( worker_alive: Bool, standing: RoadmapNodeStanding?, attempt_stale: Bool, + assignment: IssueAssignmentProjection? = none, ) -> WorkRowView { let lifecycle = work_lifecycle(rn: rn, merged: merged) let scheduling = work_scheduling(rn: rn, nodes: nodes, edges: edges) @@ -1039,6 +1045,7 @@ fn work_row_view( rounds: rounds, worker_alive: worker_alive, standing: standing, + assignment: assignment, attempt_stale: attempt_stale, } } @@ -1169,13 +1176,13 @@ fn row_activity_erred(row: WorkRowView) -> Bool { } } -fn project_subtree_errored(rows: List, edges: List, members: List, node_id: String, fuel: Int) -> Bool { +fn project_subtree_errored(rows: List, edges: List, members: List, node_id: String, fuel: Int, memberships: List = declared_roadmap_memberships()) -> Bool { if fuel <= 0 { false } else { - any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id), r => + any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id, memberships: memberships), r => row_activity_erred(row: r) - || project_subtree_errored(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1)) + || project_subtree_errored(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1, memberships: memberships)) } } @@ -1659,63 +1666,9 @@ type WorkFamilyView { rows: List } -// PROJECT HIERARCHY (operator request 2026-09-20: "see what projects are being worked on at a -// glance, expand them to see progress bars"). A PROJECT is a rendered row with at least one -// rendered TASK, and a row's tasks are its PREREQUISITES — the derivation direction is the edge -// law's, never its inverse: edge(child: X, parent: Y) says X depends on Y and Y must close before -// X launches (roadmap_spawner.node_unmet_parent_ids), so display_children(P) = -// roadmap_parent_ids(edges, P) restricted to rows this page actually renders. The exemplar is the -// arc shell-typed-invocation: the authority declares edge(child: "shell-typed-invocation", parent: -// "shell-dag-X") for each of its ten migration tasks, so the arc is the project and the ten -// shell-dag-* rows are its tasks. Nesting is RECURSIVE — a task that itself has rendered -// prerequisites is an intermediate project (the shell-residue-zero → shell-gate-migration → -// shell-effectplan-to-bash chain above the arc). -// -// The placement rules, each a pure function of the decided rows and the declared edges: -// RENDERED-ONLY — a task joins a tree (and a progress denominator) only if its row renders -// somewhere today: active work (the Active band) or a scheduling band. An unplanned prerequisite -// renders nowhere today, so counting it would report invisible work (the focus law's "an edge -// survives only when both endpoints render" applied to progress). FAMILY-FIRST — a family member -// never nests and never counts: the closing-contract family is the established presentation -// authority for generated task rows (typed membership, stated-once claim), and edge-nesting it -// under its gated node would double-render it. GROUP-EVERYWHERE (operator feedback 2026-09-21, -// amending ACTIVE-SURFACES: "I'm not seeing the grouping") — grouping is the organizing principle -// in EVERY band, including the Active band: a task row nests under its project row REGARDLESS of -// attempts, keeping its full hero treatment (stage strip, live pulse) in its nested position. The -// flattened first cut pushed every row with a live attempt out of its tree into a flat Active -// band, which is exactly why the shell→dag batch read as ten loose rows; instead, a top-level row -// now joins the Active band when its SUBTREE carries live work (itself or any display -// descendant), so the Active band's top level is projects — auto-open by the live-subtree pulse, -// red-flagged by subtree_error — plus genuinely projectless rows. Every row still renders exactly -// once: the nested position is the only position. PRIMARY-PARENT — a task that is a prerequisite -// of several rendered projects renders under the FIRST one in edge declaration order (never -// duplicated), while counting toward every project's progress. roadmap_primary_parent was -// consulted and does NOT fit: it selects a node's own first PREREQUISITE, the opposite direction -// of the display parent needed here. NOTHING-VANISHES — a row whose display-parent chain does not -// reach a top-level entry (only possible on a cycle, which the authority's acyclicity witness -// refuses upstream) renders as an ordinary top-level row, so no input can make a row disappear; -// the fuel parameters bound the recursion but never bind on an acyclic graph. -// -// Closed = accepted or superseded (node_closes_dependency), read off the row's DECIDED lifecycle: -// DoneLifecycle and SupersededLifecycle close; ReviewLifecycle — a merged PR without an accepting -// signoff — does not. "Has a PR" is never progress. -// -// SINGLE-CHILD CHAIN COLLAPSE (operator ruling 2026-09-21): a project with EXACTLY ONE display -// child which is itself a project collapses — the chain renders as one project row carrying the -// TOPMOST project's identity and headline, whose tasks and k-of-n counts are the chain END's -// display children. The exemplar is the reason: shell-residue-zero → shell-gate-migration → -// shell-effectplan-to-bash → shell-typed-invocation are four single-child links, so the arc's ten -// shell-dag-* tasks sat two disclosures deep; collapsed, the residue-zero row carries the arc's -// bar and its ten tasks render one click down. Multi-child projects never collapse. A project -// collapses THROUGH a child only it OWNS (the child's primary display parent is the collapsing -// project): fleet-closed-loop-converge's single display child is shell-gate-migration, whose -// primary parent is shell-residue-zero (first declared edge), so fleet-closed-loop-converge does -// not collapse — its "k of 1" stays honest and gate-migration joins exactly one chain. The -// swallowed intermediates are not lost: the collapsed row carries them as its CHAIN (rendered as -// the chain line inside the task disclosure, identity plus lifecycle chip each), so every node -// still renders exactly once. The depth cap counts collapsed rows once and applies after -// collapsing — a collapsed chain is one level, its tasks the next. - +// Project hierarchy follows the same recursive membership as alignment and fanin. Blocking +// determines scheduling only. Counts derive from rendered members; neither a shared prerequisite +// nor a second display of an issue creates another owned task. type ProjectTreeEntry = ProjectLeaf { row: WorkRowView } | ProjectNested { project: ProjectTreeView } @@ -1765,63 +1718,43 @@ fn row_is_family_member(members: List, node_id: String) -> } } -fn project_task_rows(rows: List, edges: List, members: List, node_id: String) -> List { - fold(edges, init: [] as List, f: (acc, e) => - if (e.child as String) == node_id && !((e.parent as String) == node_id) { - match rows |> filter(r => - r.node_id == (e.parent as String) - && row_is_rendered(row: r) - && !row_is_family_member(members: members, node_id: r.node_id)) |> first { - Absent => acc - Present { value: r } => - if any(acc, x => x.node_id == r.node_id) { acc } else { append(acc, items: [r]) } - } - } else { - acc - }) +fn project_task_rows(rows: List, edges: List, members: List, node_id: String, memberships: List = declared_roadmap_memberships()) -> List { + flat_map(project_member_ids(arc: node_id, memberships: memberships), id => + if node_project_membership(node_id: id, memberships: memberships) != MembershipMember { arc: node_id } { [] } + else { filter(rows, r => r.node_id == id && row_is_rendered(row: r) && !row_is_family_member(members: members, node_id: id)) }) } -fn row_display_parent_id(rows: List, edges: List, members: List, node_id: String) -> String? { - fold(edges, init: none, f: (acc, e) => match acc { - Present { value: v } => Present { value: v } - Absent => - if (e.parent as String) == node_id - && !((e.child as String) == node_id) - && any(rows, r => - r.node_id == (e.child as String) - && row_is_rendered(row: r) - && !row_is_family_member(members: members, node_id: r.node_id)) { - Present { value: e.child as String } - } else { - none - } - }) +fn row_display_parent_id(rows: List, edges: List, members: List, node_id: String, memberships: List = declared_roadmap_memberships()) -> String? { + match node_project_membership(node_id: node_id, memberships: memberships) { + MembershipMember { arc } => if any(rows, r => r.node_id == arc && row_is_rendered(row: r) && !row_is_family_member(members: members, node_id: arc)) { Present { value: arc } } else { none } + _ => none + } } -fn row_nest_chain_reaches_top(rows: List, edges: List, members: List, node_id: String, fuel: Int) -> Bool { +fn row_nest_chain_reaches_top(rows: List, edges: List, members: List, node_id: String, fuel: Int, memberships: List = declared_roadmap_memberships()) -> Bool { if fuel <= 0 { false } else { - match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: node_id) { + match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: node_id, memberships: memberships) { Absent => true Present { value: p } => - row_nest_chain_reaches_top(rows: rows, edges: edges, members: members, node_id: p, fuel: fuel - 1) + row_nest_chain_reaches_top(rows: rows, edges: edges, members: members, node_id: p, fuel: fuel - 1, memberships: memberships) } } } -fn row_is_nested(rows: List, edges: List, members: List, node_id: String) -> Bool { - match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: node_id) { +fn row_is_nested(rows: List, edges: List, members: List, node_id: String, memberships: List = declared_roadmap_memberships()) -> Bool { + match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: node_id, memberships: memberships) { Absent => false Present { value: _ } => - row_nest_chain_reaches_top(rows: rows, edges: edges, members: members, node_id: node_id, fuel: count(rows)) + row_nest_chain_reaches_top(rows: rows, edges: edges, members: members, node_id: node_id, fuel: count(rows), memberships: memberships) } } -fn row_is_project(rows: List, edges: List, members: List, row: WorkRowView) -> Bool { +fn row_is_project(rows: List, edges: List, members: List, row: WorkRowView, memberships: List = declared_roadmap_memberships()) -> Bool { row_is_rendered(row: row) && !row_is_family_member(members: members, node_id: row.node_id) - && count(project_task_rows(rows: rows, edges: edges, members: members, node_id: row.node_id)) > 0 + && count(project_task_rows(rows: rows, edges: edges, members: members, node_id: row.node_id, memberships: memberships)) > 0 } fn row_pulses(row: WorkRowView) -> Bool { @@ -1844,13 +1777,13 @@ fn row_pulses(row: WorkRowView) -> Bool { } } -fn project_subtree_pulses(rows: List, edges: List, members: List, node_id: String, fuel: Int) -> Bool { +fn project_subtree_pulses(rows: List, edges: List, members: List, node_id: String, fuel: Int, memberships: List = declared_roadmap_memberships()) -> Bool { if fuel <= 0 { false } else { - any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id), r => + any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id, memberships: memberships), r => row_pulses(row: r) - || project_subtree_pulses(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1)) + || project_subtree_pulses(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1, memberships: memberships)) } } @@ -1868,21 +1801,21 @@ fn project_subtree_pulses(rows: List, edges: List, mem // two chains). The walk stops at the chain end: a node with zero or several display children, or // one whose single display child is a leaf or owned elsewhere. chain carries every swallowed // project INCLUDING the end; an uncollapsed project has chain [] and end == its own row. -fn project_chain_walk(row: WorkRowView, rows: List, edges: List, members: List, fuel: Int) -> ProjectChainWalk { +fn project_chain_walk(row: WorkRowView, rows: List, edges: List, members: List, fuel: Int, memberships: List = declared_roadmap_memberships()) -> ProjectChainWalk { if fuel <= 0 { ProjectChainWalk { end: row, chain: [] } } else { - let disp = project_task_rows(rows: rows, edges: edges, members: members, node_id: row.node_id) + let disp = project_task_rows(rows: rows, edges: edges, members: members, node_id: row.node_id, memberships: memberships) if count(disp) == 1 { match disp.first() { Absent => ProjectChainWalk { end: row, chain: [] } Present { value: c } => - if row_is_project(rows: rows, edges: edges, members: members, row: c) - && (match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: c.node_id) { + if row_is_project(rows: rows, edges: edges, members: members, row: c, memberships: memberships) + && (match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: c.node_id, memberships: memberships) { Present { value: p } => p == row.node_id Absent => false }) { - let rest = project_chain_walk(row: c, rows: rows, edges: edges, members: members, fuel: fuel - 1) + let rest = project_chain_walk(row: c, rows: rows, edges: edges, members: members, fuel: fuel - 1, memberships: memberships) ProjectChainWalk { end: rest.end, chain: concat([c], rest.chain) } } else { ProjectChainWalk { end: row, chain: [] } @@ -1894,38 +1827,35 @@ fn project_chain_walk(row: WorkRowView, rows: List, edges: List, edges: List, members: List, depth: Int, fuel: Int) -> ProjectTreeView { - let walk = project_chain_walk(row: row, rows: rows, edges: edges, members: members, fuel: fuel) +fn project_tree_view(row: WorkRowView, rows: List, edges: List, members: List, depth: Int, fuel: Int, memberships: List = declared_roadmap_memberships()) -> ProjectTreeView { + let walk = project_chain_walk(row: row, rows: rows, edges: edges, members: members, fuel: fuel, memberships: memberships) let end = walk.end - let tasks = project_task_rows(rows: rows, edges: edges, members: members, node_id: end.node_id) + let tasks = project_task_rows(rows: rows, edges: edges, members: members, node_id: end.node_id, memberships: memberships) let nested = if fuel <= 0 { [] } else { tasks |> filter(t => row_is_rendered(row: t) - && (match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: t.node_id) { + && (match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: t.node_id, memberships: memberships) { Present { value: p } => p == end.node_id Absent => false })) } - let linked = tasks |> filter(t => - match row_display_parent_id(rows: rows, edges: edges, members: members, node_id: t.node_id) { - Present { value: p } => !(p == end.node_id) - Absent => false - }) + let linked = filter(rows, r => any(edges, e => e.child as String == end.node_id && e.parent as String == r.node_id) + && !any(tasks, t => t.node_id == r.node_id)) ProjectTreeView { row: row, chain: walk.chain, depth: depth, closed_count: count(tasks |> filter(t => row_is_closed(row: t))), total_count: count(tasks), - auto_open: project_subtree_pulses(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows)), + auto_open: project_subtree_pulses(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows), memberships: memberships), subtree_error: row_activity_erred(row: row) - || project_subtree_errored(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows)), + || project_subtree_errored(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows), memberships: memberships), linked: linked, entries: map(nested, t => - if row_is_project(rows: rows, edges: edges, members: members, row: t) { - ProjectNested { project: project_tree_view(row: t, rows: rows, edges: edges, members: members, depth: depth + 1, fuel: fuel - 1) } + if row_is_project(rows: rows, edges: edges, members: members, row: t, memberships: memberships) { + ProjectNested { project: project_tree_view(row: t, rows: rows, edges: edges, members: members, depth: depth + 1, fuel: fuel - 1, memberships: memberships) } } else { ProjectLeaf { row: t } }), @@ -2031,16 +1961,10 @@ type IssueLinkView { headline: String } -// PARENT AND BLOCKING STAY SEPARATE (owner ruling 2026-09-24): the panel carries DECOMPOSITION -// (parent — where the issue nests) and DEPENDENCY (blocking / blocked-by — what it obstructs and -// what obstructs it) as distinct fields, never one merged list. THE NAMED GAP, not papered over: -// the roadmap graph carries ONLY dependency edges today (the chain-ambiguity finding), so the -// decomposition side rides the tree derivation's primary-parent rule — the first edge target read -// as the display parent — which is a presentation approximation, not a modeled decomposition -// relation. When decomposition edges land, the parent field rebinds to them and the blocking -// fields stay exactly as they are. +// Nesting and blocking are independent dimensions with one shared ownership authority. type IssueLinkPanel { parent: IssueLinkView? + children: List blocking: List blocked_by: List } @@ -2056,15 +1980,13 @@ fn child_nodes_of(nodes: List, edges: List, parent: Ro any(roadmap_parent_ids(edges: edges, node: n.node), p => p == parent)) } -fn issue_link_panel(rn: RoadmapNode, nodes: List, edges: List) -> IssueLinkPanel { +fn issue_link_panel(rn: RoadmapNode, nodes: List, edges: List, memberships: List = declared_roadmap_memberships()) -> IssueLinkPanel { IssueLinkPanel { - parent: match roadmap_primary_parent(edges: edges, node: rn.node) { - TopLevel => none - UnderParent { parent: pid } => match find_node(nodes: nodes, node_id: pid as String) { - Present { value: p } => Present { value: issue_link_view_of(rn: p) } - Absent => none - } + parent: match node_project_membership(node_id: rn.node as String, memberships: memberships) { + MembershipMember { arc } => match find_node(nodes: nodes, node_id: arc) { Present { value: p } => Present { value: issue_link_view_of(rn: p) } Absent => none } + _ => none }, + children: flat_map(project_member_ids(arc: rn.node as String, memberships: memberships), id => match find_node(nodes: nodes, node_id: id) { Present { value: child } => [issue_link_view_of(rn: child)] Absent => [] }), blocking: map(child_nodes_of(nodes: nodes, edges: edges, parent: rn.node), issue_link_view_of), blocked_by: flat_map(scheduling_blockers(rn: rn, nodes: nodes, edges: edges), pid => match find_node(nodes: nodes, node_id: pid) { @@ -2159,38 +2081,38 @@ fn row_is_active_work(row: WorkRowView) -> Bool { // in its display subtree — itself or any descendant — so a project whose tasks are being worked // surfaces as ONE grouped row at the top, with the live tasks nested inside it. Family members // keep their established law: active work leaves the group for the Active band as a flat row. -fn project_subtree_active(rows: List, edges: List, members: List, node_id: String, fuel: Int) -> Bool { +fn project_subtree_active(rows: List, edges: List, members: List, node_id: String, fuel: Int, memberships: List = declared_roadmap_memberships()) -> Bool { if fuel <= 0 { false } else { - any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id), r => + any(project_task_rows(rows: rows, edges: edges, members: members, node_id: node_id, memberships: memberships), r => row_is_active_work(row: r) - || project_subtree_active(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1)) + || project_subtree_active(rows: rows, edges: edges, members: members, node_id: r.node_id, fuel: fuel - 1, memberships: memberships)) } } -fn row_subtree_active(rows: List, edges: List, members: List, row: WorkRowView) -> Bool { +fn row_subtree_active(rows: List, edges: List, members: List, row: WorkRowView, memberships: List = declared_roadmap_memberships()) -> Bool { row_is_active_work(row: row) - || project_subtree_active(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows)) + || project_subtree_active(rows: rows, edges: edges, members: members, node_id: row.node_id, fuel: count(rows), memberships: memberships) } -fn row_is_active_root(rows: List, edges: List, members: List, row: WorkRowView) -> Bool { +fn row_is_active_root(rows: List, edges: List, members: List, row: WorkRowView, memberships: List = declared_roadmap_memberships()) -> Bool { !row_is_family_member(members: members, node_id: row.node_id) - && !row_is_nested(rows: rows, edges: edges, members: members, node_id: row.node_id) - && row_subtree_active(rows: rows, edges: edges, members: members, row: row) + && !row_is_nested(rows: rows, edges: edges, members: members, node_id: row.node_id, memberships: memberships) + && row_subtree_active(rows: rows, edges: edges, members: members, row: row, memberships: memberships) } -fn rows_for_active_band(rows: List, edges: List, members: List) -> List { +fn rows_for_active_band(rows: List, edges: List, members: List, memberships: List = declared_roadmap_memberships()) -> List { rows |> filter(r => (row_is_family_member(members: members, node_id: r.node_id) && row_is_active_work(row: r)) - || row_is_active_root(rows: rows, edges: edges, members: members, row: r)) + || row_is_active_root(rows: rows, edges: edges, members: members, row: r, memberships: memberships)) } -fn rows_for_scheduling_band(rows: List, edges: List, members: List, key: String) -> List { +fn rows_for_scheduling_band(rows: List, edges: List, members: List, key: String, memberships: List = declared_roadmap_memberships()) -> List { rows |> filter(r => work_scheduling_key(s: r.scheduling) == key && !(row_is_family_member(members: members, node_id: r.node_id) && row_is_active_work(row: r)) - && !row_is_active_root(rows: rows, edges: edges, members: members, row: r)) + && !row_is_active_root(rows: rows, edges: edges, members: members, row: r, memberships: memberships)) } fn family_member_for(members: List, node_id: String) -> WorkFamilyMemberFact? { @@ -2215,6 +2137,7 @@ fn family_subject_row(row: WorkRowView, subject: String) -> WorkRowView { rounds: row.rounds, worker_alive: row.worker_alive, standing: row.standing, + assignment: row.assignment, attempt_stale: row.attempt_stale, } } @@ -2235,16 +2158,16 @@ fn band_entries( edges: List, families: List, members: List, -) -> List { + memberships: List = declared_roadmap_memberships()) -> List { let grouped = fold(rows, init: BandEntriesAcc { entries: [], emitted_family_ids: [] }, f: (acc, r) => match family_member_for(members: members, node_id: r.node_id) { Absent => - if row_is_nested(rows: all_rows, edges: edges, members: members, node_id: r.node_id) { + if row_is_nested(rows: all_rows, edges: edges, members: members, node_id: r.node_id, memberships: memberships) { acc - } else if row_is_project(rows: all_rows, edges: edges, members: members, row: r) { + } else if row_is_project(rows: all_rows, edges: edges, members: members, row: r, memberships: memberships) { BandEntriesAcc { entries: append(acc.entries, items: [BandProject { project: project_tree_view( - row: r, rows: all_rows, edges: edges, members: members, depth: 1, fuel: count(all_rows)) }]), + row: r, rows: all_rows, edges: edges, members: members, depth: 1, fuel: count(all_rows), memberships: memberships) }]), emitted_family_ids: acc.emitted_family_ids, } } else { @@ -2295,12 +2218,12 @@ fn daily_workspace_view( observation: ObservationImpactView, families: List, members: List, -) -> DailyWorkspaceView { - let active_entries = band_entries(rows: rows_for_active_band(rows: rows, edges: edges, members: members), all_rows: rows, edges: edges, families: [], members: []) - let ready_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "ready"), all_rows: rows, edges: edges, families: families, members: members) - let upcoming_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "upcoming"), all_rows: rows, edges: edges, families: families, members: members) - let done_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "done"), all_rows: rows, edges: edges, families: families, members: members) - let superseded_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "superseded"), all_rows: rows, edges: edges, families: families, members: members) + memberships: List = declared_roadmap_memberships()) -> DailyWorkspaceView { + let active_entries = band_entries(rows: rows_for_active_band(rows: rows, edges: edges, members: members, memberships: memberships), all_rows: rows, edges: edges, families: [], members: [], memberships: memberships) + let ready_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "ready", memberships: memberships), all_rows: rows, edges: edges, families: families, members: members, memberships: memberships) + let upcoming_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "upcoming", memberships: memberships), all_rows: rows, edges: edges, families: families, members: members, memberships: memberships) + let done_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "done", memberships: memberships), all_rows: rows, edges: edges, families: families, members: members, memberships: memberships) + let superseded_entries = band_entries(rows: rows_for_scheduling_band(rows: rows, edges: edges, members: members, key: "superseded", memberships: memberships), all_rows: rows, edges: edges, families: families, members: members, memberships: memberships) DailyWorkspaceView { program: program, observation: observation, diff --git a/dag/gunbc/roadmap/roadmap_project.dag b/dag/gunbc/roadmap/roadmap_project.dag index 093e9ad43ae..9c99e14d79a 100644 --- a/dag/gunbc/roadmap/roadmap_project.dag +++ b/dag/gunbc/roadmap/roadmap_project.dag @@ -1,30 +1,19 @@ module gunbc.roadmap.roadmap_project -import std.types { String, List } -import gunbc.roadmap_model { RoadmapEdge } +import std.types { String, List, Bool } +import gunbc.roadmap_nesting { RoadmapMembership } -// PROJECT MEMBERSHIP IS DERIVED FROM THE DEPENDENCY EDGES, NEVER ROSTERED (owner ruling -// 2026-09-20: the 10-task batch integrates via native gunbc SCM fanin). The roadmap authority -// already declares the only parent relationship it has: edge(child: arc, parent: task) means the -// arc depends on the task -- the task is a prerequisite of the arc. So a node's project is the arc -// that depends on it, and a node no arc depends on is a project of one, which is the degenerate -// delivery case docs/plans/harness-work-lifecycle.md declares, not a special case. -// -// A HAND-MAINTAINED ROSTER BESIDE THIS DERIVATION IS THE FORK THIS MODULE EXISTS TO REFUSE: the -// edges are the authority, so adding an eleventh node to a batch is an edge edit and nothing else. -// AMBIGUOUS IS A TYPED STATE, NOT A DEFAULT. Two arcs depending on one node would put its candidate -// in two projects' integration heads -- the fanin cannot choose, and choosing (first declared, -// lexicographically least) would be a heuristic standing in for an observation. The state refuses: -// the authority must say which arc owns the delivery. +// One independently authored nesting relation serves UI, alignment, supervision and fanin. +// A shared prerequisite is not a member of each project that it blocks. type ProjectMembership = MembershipStandalone | MembershipMember { arc: String } | MembershipAmbiguous { arcs: List } -fn node_project_membership(node_id: String, edges: List) -> ProjectMembership { - let arcs = edges - |> filter(e => (e.parent as String) == node_id) - |> map(e => e.child as String) +fn node_project_membership(node_id: String, memberships: List) -> ProjectMembership { + let arcs = memberships + |> filter(e => e.member == node_id) + |> map(e => e.project) if count(arcs) == 0 { MembershipStandalone } else if count(arcs) == 1 { @@ -41,19 +30,35 @@ fn node_project_membership(node_id: String, edges: List) -> Project // project, one envelope, one integration head -- and the key is derived by the same rule every // consumer uses, so capture, fanin and publication cannot disagree about which envelope a // candidate belongs to. Ambiguous membership has no key: there is no honest envelope to name. -fn project_key_for_node(node_id: String, edges: List) -> String? { - match node_project_membership(node_id: node_id, edges: edges) { - MembershipStandalone => Present { value: node_id } - MembershipMember { arc } => Present { value: arc } - MembershipAmbiguous { arcs: _ } => none +fn project_key_for_node(node_id: String, memberships: List) -> String? { + match project_root_for_node(node_id: node_id, memberships: memberships) { + Absent => none + Present { value: _ } => match node_project_membership(node_id: node_id, memberships: memberships) { + MembershipStandalone => Present { value: node_id } + MembershipMember { arc } => Present { value: arc } + MembershipAmbiguous { arcs: _ } => none + } } } -// THE MEMBERSHIP OF A PROJECT, for the k-of-n integration standing: every node the arc depends -// on, in declared order. The arc itself is NOT a member -- it is the delivery, and it has its own -// project-of-one unless a further arc depends on it. -fn project_member_ids(arc: String, edges: List) -> List { - edges - |> filter(e => (e.child as String) == arc) - |> map(e => e.parent as String) +// Immediate children, in authored order. Further levels use the same relation recursively. +fn project_member_ids(arc: String, memberships: List) -> List { + memberships + |> filter(e => (e.project) == arc) + |> map(e => e.member) +} + +type ProjectRootWalk { current: String, visited: List, refused: Bool, done: Bool } + +fn project_root_for_node(node_id: String, memberships: List) -> String? { + let walked = fold(memberships, init: ProjectRootWalk { current: node_id, visited: [node_id], refused: false, done: false }, f: (acc, unused) => + if acc.done || acc.refused { acc } else { match node_project_membership(node_id: acc.current, memberships: memberships) { + MembershipStandalone => ProjectRootWalk { current: acc.current, visited: acc.visited, refused: false, done: true } + MembershipAmbiguous { arcs: _ } => ProjectRootWalk { current: acc.current, visited: acc.visited, refused: true, done: false } + MembershipMember { arc } => ProjectRootWalk { current: arc, visited: concat(acc.visited, [arc]), refused: any(acc.visited, v => v == arc), done: false } + } }) + if walked.refused { none } else { match node_project_membership(node_id: walked.current, memberships: memberships) { + MembershipStandalone => Present { value: walked.current } + _ => none + } } } diff --git a/dag/gunbc/roadmap/roadmap_sandbox.dag b/dag/gunbc/roadmap/roadmap_sandbox.dag index a3c2e0a56d6..d1537754c96 100644 --- a/dag/gunbc/roadmap/roadmap_sandbox.dag +++ b/dag/gunbc/roadmap/roadmap_sandbox.dag @@ -409,6 +409,7 @@ fn fixture_specimen_view(node_id: String, activity: ActivityView, rounds: Harnes disclosure: NoDisclosure, rounds: rounds, worker_alive: worker_alive, + assignment: none, standing: standing, attempt_stale: false, } @@ -488,6 +489,7 @@ fn fixture_ready_view(node_id: String, headline: String) -> WorkRowView { disclosure: NoDisclosure, rounds: RoundsUnobserved { reason: "fixture models no provider event stream" as NonEmptyStr }, worker_alive: false, + assignment: none, standing: none, attempt_stale: false, } diff --git a/dag/gunbc/roadmap/roadmap_serve.dag b/dag/gunbc/roadmap/roadmap_serve.dag index 758a5de5b88..10946e3fa86 100644 --- a/dag/gunbc/roadmap/roadmap_serve.dag +++ b/dag/gunbc/roadmap/roadmap_serve.dag @@ -1,5 +1,13 @@ module gunbc.roadmap_serve +import gunbc.roadmap.roadmap_event_log { StandingsObserved, NodeStandingRow } + +import gunbc.roadmap.roadmap_event_log { roadmap_issue_state } +import gunbc.roadmap_issue_command { IssueCommandSubject, AssignIssue, CreateComment, IssueFormContext, issue_command_apply, issue_operation_nonce } +import gunbc.roadmap.roadmap_event_carrier { roadmap_operation_identity_valid } + +import extdeps.http.form_urlencoded { form_decode, form_field, FormDecoded, FormRefused } + import gunbc.roadmap_allocation_location { allocation_page_path } import gunbc.roadmap_allocation_page { allocation_page, allocation_client_path } import gunbc.roadmap_allocation_client { allocation_client_source } @@ -850,30 +858,32 @@ fn serve_issue_detail_response_over( ) -> ServeHttpResponse { let node_id = path_param_value(params: params, name: "node_id") let layout = roadmap_event_carrier_layout_for_instance(instance: instance) - let standings = roadmap_standings_read(layout: layout) - let viewer = authenticated_viewer_label(evidence: request.security.authentication) - let recents = match roadmap_events_read_all(layout: layout) { - AllEventsRead { envelopes } => roadmap_recent_claimed_assignees(envs: envelopes) - AllEventsReadRefused { step: _, reason: _ } => [] - } - let envelopes = match roadmap_events_read(layout: layout, node: node_id as NonEmptyStr as RoadmapNodeId) { - EventsRead { node: _, envelopes } => envelopes - EventsReadRefused { node: _, step: _, reason: _ } => [] - } - let page = issue_detail_page_for( - proj: plan, - node_id: node_id, - attempts: [], - attempts_refused: none, - standings: standings, - launch: belt_launch_host_standing_for_page_over(instance: instance, plan: plan), - stale_nodes: [], - events: envelopes, - viewer: viewer, - recents: recents) - serve_page_response_with_status( - page: page, - status: if issue_detail_node_exists(proj: plan, node_id: node_id) { 200 } else { 404 }) + if !issue_detail_node_exists(proj: plan, node_id: node_id) { serve_text_response(status: 404, content_type: text_plain_utf8, body: "issue not found") } + else { match roadmap_events_read(layout: layout, node: node_id as NonEmptyStr as RoadmapNodeId) { + EventsReadRefused { node: _, step, reason } => serve_text_response(status: 503, content_type: text_plain_utf8, + body: join(["Issue history unavailable at ", step, ": ", reason, ". No revision-pinned mutations are available."], "")) + EventsRead { node: _, envelopes } => { + let state = roadmap_issue_state(envs: envelopes) + let standing = state.standing + let standing_key = roadmap_node_standing_key(s: standing) + if standing_key == "forked" || standing_key == "history_incomplete" { + serve_text_response(status: 503, content_type: text_plain_utf8, body: concat("Issue history unavailable: ", roadmap_node_standing_text(s: standing))) + } else { + let context = match request.security.authentication { + GoogleOidcSession { session } => IssueFormContext { operation: match issue_operation_nonce() { Present { value: nonce } => nonce Absent => "" }, csrf: session.csrf.token as String } + _ => IssueFormContext { operation: "", csrf: "" } + } + let recents = match roadmap_events_read_all(layout: layout) { AllEventsRead { envelopes: all_events } => roadmap_recent_claimed_assignees(envs: all_events) _ => [] } + let page = issue_detail_page_for(proj: plan, node_id: node_id, attempts: [], + attempts_refused: Present { value: "The per-issue attempt observation is not connected; no claim of zero attempts is made." }, + standings: StandingsObserved { rows: [NodeStandingRow { node_id: node_id, standing: standing, assignment: state.assignment }] }, + launch: belt_launch_host_standing_for_page_over(instance: instance, plan: plan), stale_nodes: [], events: envelopes, + viewer: authenticated_viewer_label(evidence: request.security.authentication), recents: recents, command_context: context) + let response = serve_page_response_with_status(page: page, status: 200) + ServeHttpResponse { status: response.status, content_type: response.content_type, body: response.body, headers: concat(response.headers, [ServeResponseHeaderNoStore]) } + } + } + } } } // THE ASSIGNEE SUBMISSION, resolved to the stable PrincipalRef the v3 event records (owner @@ -905,11 +915,13 @@ fn serve_issue_assignment_context_over( AllEventsRead { envelopes: all_events } => roadmap_recent_claimed_assignees(envs: all_events) AllEventsReadRefused { step: _, reason: _ } => [] } - serve_text_response_with_headers( - status: 200, - content_type: application_json_utf8, - body: inline_assignment_context_body(session: session, events: envelopes, recents: recents), - headers: [ServeResponseHeaderNoStore]) + match issue_operation_nonce() { + Absent => serve_text_response(status: 503, content_type: text_plain_utf8, body: "Cannot prepare a stable assignment operation. Try again before submitting.") + Present { value: nonce } => serve_text_response_with_headers( + status: 200, content_type: application_json_utf8, + body: inline_assignment_context_body(session: session, events: envelopes, recents: recents, operation: nonce), + headers: [ServeResponseHeaderNoStore]) + } } } AuthAbsent => serve_text_response(status: 401, content_type: text_plain_utf8, body: "Sign in to assign this issue.") @@ -967,7 +979,7 @@ fn assignee_lookup_refusal(raw: String) -> String { // never "assignment failed". RE-ASSIGNING THE SAME REVISION joins the existing request: the // admission's own already-live / already-accepted arms refuse the duplicate attempt, and the // response says the assignment stands beside the refusal. THE REQUEST BODY is the form's -// text/plain encoding — newline-delimited k=v fields, no percent-encoding to undo. +// application/x-www-form-urlencoded encoding, decoded once by the shared strict codec. fn serve_issue_assign_response_over( instance: HostDashboardInstance, plan: RoadmapAuthorityProjection, @@ -975,79 +987,49 @@ fn serve_issue_assign_response_over( params: List, ) -> ServeHttpResponse { let node_id = path_param_value(params: params, name: "node_id") - if !issue_detail_node_exists(proj: plan, node_id: node_id) { - serve_text_response(status: 404, content_type: text_plain_utf8, body: join(["issue not found: ", node_id, "\n"], "")) - } else { - let observed_at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } - match human_write_admission( - route_path: join(["/issue/", node_id, "/assign"], "") as NonEmptyStr, - action: ServeActionAssignIssue, - security: request.security, - authorization_policy: tracker_write_authorization_policy, - deployment_policy: roadmap_deployment_access_policy, - csrf_presented: issue_form_field(body: request.body, name: "csrf"), - observed_at: observed_at, - ) { - Deny { refusal } => - serve_text_response(status: 403, content_type: text_plain_utf8, body: concat("assignment refused: ", access_refusal_reason(refusal: refusal))) - Permit => - let author = authenticated_write_author_label(security: request.security) - let raw_assignee = issue_form_field(body: request.body, name: "assignee") - if !assignee_submission_shape_valid(s: raw_assignee) { - serve_text_response(status: 400, content_type: text_plain_utf8, body: join(["assignment refused: '", raw_assignee, "' is not an email spelling, the fabric selector, or a principal spelling\n"], "")) - } else { - let raw_return_to = issue_form_field(body: request.body, name: "return_to") - if raw_return_to != "" && !assignee_submission_shape_valid(s: raw_return_to) { - serve_text_response(status: 400, content_type: text_plain_utf8, body: join(["assignment refused: return_to '", raw_return_to, "' is not an email spelling, the fabric selector, or a principal spelling\n"], "")) - } else { - let layout = roadmap_event_carrier_layout_for_instance(instance: instance) - let nid = node_id as NonEmptyStr as RoadmapNodeId - match roadmap_events_read(layout: layout, node: nid) { - EventsReadRefused { node: _, step, reason } => - serve_text_response(status: 503, content_type: text_plain_utf8, body: join(["assignment refused: could not read the issue's history (", step, "): ", reason, "\n"], "")) - EventsRead { node: _, envelopes } => - let standing = roadmap_node_standing(envs: envelopes) - let key = roadmap_node_standing_key(s: standing) - if key == "forked" || key == "history_incomplete" { - serve_text_response(status: 409, content_type: text_plain_utf8, body: join(["assignment refused: the issue's history is ", key, ": ", roadmap_node_standing_text(s: standing), "\n"], "")) - } else { - let heads = filter(envelopes, e => !any(envelopes, o => match o.event.parent { Present { value: p } => (p as String) == (e.id as String) Absent => false })) - let head = match heads.first() { Present { value: h } => h.id as String Absent => "" } - let expected_revision = issue_form_field(body: request.body, name: "expected_revision") - if expected_revision != head { - serve_text_response(status: 409, content_type: text_plain_utf8, body: join(["assignment refused: the issue changed since the page rendered (expected revision ", expected_revision, ", head ", head, ") — reload and assign again\n"], "")) - } else { - let at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } - match authenticated_write_author_principal(security: request.security) { - Absent => - serve_text_response(status: 403, content_type: text_plain_utf8, body: "assignment refused: admitted write carried no author principal (unreachable past the write law)\n") - Present { value: author_ref } => - match assignee_principal_of(raw: raw_assignee) { - Absent => - serve_text_response(status: 400, content_type: text_plain_utf8, body: join(["assignment refused: ", assignee_lookup_refusal(raw: raw_assignee), "\n"], "")) - Present { value: assignee_ref } => - match (if raw_return_to == "" { Present { value: author_ref } } else { assignee_principal_of(raw: raw_return_to) }) { - Absent => - serve_text_response(status: 400, content_type: text_plain_utf8, body: join(["assignment refused: return_to ", assignee_lookup_refusal(raw: raw_return_to), "\n"], "")) - Present { value: return_to_ref } => - match roadmap_event_append(layout: layout, event: RoadmapEvent { node: nid, author: EventPrincipalAuthenticated { ref: author_ref }, parent: match heads.first() { Present { value: h } => Present { value: h.id } Absent => none }, kind: Claimed { assignee: EventPrincipalAuthenticated { ref: assignee_ref }, return_to: EventPrincipalAuthenticated { ref: return_to_ref } }, recorded_at: at }) { - EventAppended { id } => - serve_assignment_launch_follow_through(instance: instance, plan: plan, node_id: node_id, assignee: assignee_ref, event_id: id as String) - EventAlreadyPresent { id } => - serve_assignment_launch_follow_through(instance: instance, plan: plan, node_id: node_id, assignee: assignee_ref, event_id: id as String) - EventAppendRefused { id: _, step, reason } => - serve_text_response(status: 503, content_type: text_plain_utf8, body: join(["assignment refused at ", step, ": ", reason, "\n"], "")) - } - } - } - } - } + let observed_at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } + if !issue_detail_node_exists(proj: plan, node_id: node_id) { serve_text_response(status: 404, content_type: text_plain_utf8, body: "issue not found") } + else { match human_write_admission(route_path: concat("/issue/", concat(node_id, "/assign")) as NonEmptyStr, + action: ServeActionAssignIssue, security: request.security, authorization_policy: tracker_write_authorization_policy, + deployment_policy: roadmap_deployment_access_policy, csrf_presented: issue_form_field(body: request.body, name: "csrf"), observed_at: observed_at) { + Deny { refusal } => serve_text_response(status: 403, content_type: text_plain_utf8, body: access_refusal_reason(refusal: refusal)) + Permit => if !issue_command_form_valid(body: request.body) { serve_text_response(status: 400, content_type: text_plain_utf8, body: "assignment refused: a valid operation key and explicit expected_revision are required") } + else { match authenticated_write_author_principal(security: request.security) { + Absent => serve_text_response(status: 403, content_type: text_plain_utf8, body: "authenticated principal required") + Present { value: author } => match assignee_principal_of(raw: issue_form_field(body: request.body, name: "assignee")) { + Absent => serve_text_response(status: 400, content_type: text_plain_utf8, body: "assignee has no admitted principal") + Present { value: assignee } => { + let target = issue_form_field(body: request.body, name: "return_to") + match (if target == "" { Present { value: author } } else { assignee_principal_of(raw: target) }) { + Absent => serve_text_response(status: 400, content_type: text_plain_utf8, body: "return_to has no admitted principal") + Present { value: return_to } => { + let expected = issue_form_field(body: request.body, name: "expected_revision") + let outcome = issue_command_apply(layout: roadmap_event_carrier_layout_for_instance(instance: instance), + command: AssignIssue { subject: IssueCommandSubject { node: node_id as NonEmptyStr as RoadmapNodeId, + author: EventPrincipalAuthenticated { ref: author }, operation: issue_form_field(body: request.body, name: "key") as NonEmptyStr, + expected: if expected == "" { none } else { Present { value: expected as NonEmptyStr as RoadmapEventId } } }, + assignee: EventPrincipalAuthenticated { ref: assignee }, return_to: EventPrincipalAuthenticated { ref: return_to } }, recorded_at: observed_at) + match outcome { + EventAppended { id } => serve_assignment_launch_follow_through(instance: instance, plan: plan, node_id: node_id, assignee: assignee, event_id: id as String) + EventAlreadyPresent { id } => serve_text_response(status: 200, content_type: text_plain_utf8, body: join(["assigned: ", node_id, " (already recorded event ", id as String, ")\n"], "")) + EventAppendRefused { id: _, step, reason } => issue_command_refusal_response(step: step, reason: reason) } + } } } } - } - } + } } + } } +} + +fn issue_command_form_valid(body: String) -> Bool { + issue_form_has_field(body: body, name: "expected_revision") + && roadmap_operation_identity_valid(operation: issue_form_field(body: body, name: "key")) +} + +fn issue_command_refusal_response(step: String, reason: String) -> ServeHttpResponse { + serve_text_response(status: if step == "revision-conflict" || step == "operation-conflict" { 409 } else { 503 }, + content_type: text_plain_utf8, body: join(["issue command refused at ", step, ": ", reason, "\n"], "")) } // THE AUTHOR PRINCIPAL OF AN ADMITTED WRITE, the typed form the v3 event records: a Google OIDC @@ -1075,19 +1057,18 @@ fn authenticated_write_author_label(security: RequestSecurityContext String { - let prefix = join([name, "="], "") - match (filter(split(s: body, delimiter: "\n"), line => starts_with(s: line, prefix: prefix))).first() { - Present { value: line } => substring(s: line, start: string_length(s: prefix), end: string_length(s: line)) + match form_field(form: form_decode(wire: body), name: name) { + Present { value } => value Absent => "" } } +fn issue_form_has_field(body: String, name: String) -> Bool { + match form_field(form: form_decode(wire: body), name: name) { Present { value: _ } => true Absent => false } +} + // THE EXECUTION ENTRY POINT'S FOLLOW-THROUGH, factored pure over the admission outcome so the // witness drives it with constructed outcomes: a human assignee stops at the recorded fact (NEVER // dispatches); the fabric endpoint drives the SAME admission the /dispatch route answers and the @@ -1126,94 +1107,49 @@ fn serve_fabric_assignment_response(node_id: String, assignee: PrincipalRef, eve } } -// THE COMMENT ROUTE (owner mandate 2026-09-25: comments are durable events; retries never -// duplicate): POST /issue//comment appends a CommentCreated to the SAME chained, fork-refusing -// log — the event-CLI idiom again (read, refuse on forked/incomplete, parent = the single head, -// probed clock, append through the carrier). The body ships enctype text/plain with the key line -// first and the body LAST (markdown freely carries newlines). The key is the client's idempotency -// token: already present, the request is a RETRY — 200 with the existing comment's deep link and -// NOTHING appended (control 7: retries never duplicate; this is the duplicate-append-refused -// arm). A composer that minted no key (the no-JS native form) still never duplicates a retry on -// an unchanged history: same author, same body, same head is the retry. Visibility is recorded -// ("public" this cut). The author is the AUTHENTICATED PRINCIPAL — the same write law as the -// assign route: Google OIDC session + action authorization + CSRF, with the event recording the -// session principal's stable label; tailnet transport alone refuses at the authentication axis. -// The expected_revision guard matches the assign route — 409 STALE on a moved head. +// Comment commands use the same strict URL-encoded codec as native and enhanced assignment. +// Every form has a stable operation identity, explicit expected revision, and session CSRF. +// A retry returns the original event and comment ordinal, even after intervening events; +// equal text with another operation identity is an intentional second comment. fn serve_issue_comment_response_over( instance: HostDashboardInstance, request: ServeHttpRequest, params: List, ) -> ServeHttpResponse { let node_id = path_param_value(params: params, name: "node_id") - let plan = roadmap_authority_projection() - if !issue_detail_node_exists(proj: plan, node_id: node_id) { - serve_text_response(status: 404, content_type: text_plain_utf8, body: join(["issue not found: ", node_id, "\n"], "")) - } else { - let observed_at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } - match human_write_admission( - route_path: join(["/issue/", node_id, "/comment"], "") as NonEmptyStr, - action: ServeActionCommentIssue, - security: request.security, - authorization_policy: tracker_write_authorization_policy, - deployment_policy: roadmap_deployment_access_policy, - csrf_presented: issue_form_field(body: request.body, name: "csrf"), - observed_at: observed_at, - ) { - Deny { refusal } => - serve_text_response(status: 403, content_type: text_plain_utf8, body: concat("comment refused: ", access_refusal_reason(refusal: refusal))) - Permit => - let author_principal = authenticated_write_author_principal(security: request.security) - let body = issue_comment_body_of(wire: request.body) - if trim(body) == "" { - serve_text_response(status: 400, content_type: text_plain_utf8, body: "comment refused: the body is empty\n") - } else { - let layout = roadmap_event_carrier_layout_for_instance(instance: instance) - let nid = node_id as NonEmptyStr as RoadmapNodeId - match roadmap_events_read(layout: layout, node: nid) { - EventsReadRefused { node: _, step, reason } => - serve_text_response(status: 503, content_type: text_plain_utf8, body: join(["comment refused: could not read the issue's history (", step, "): ", reason, "\n"], "")) - EventsRead { node: _, envelopes } => - let standing = roadmap_node_standing(envs: envelopes) - let key = roadmap_node_standing_key(s: standing) - if key == "forked" || key == "history_incomplete" { - serve_text_response(status: 409, content_type: text_plain_utf8, body: join(["comment refused: the issue's history is ", key, ": ", roadmap_node_standing_text(s: standing), "\n"], "")) - } else { - let heads = filter(envelopes, e => !any(envelopes, o => match o.event.parent { Present { value: p } => (p as String) == (e.id as String) Absent => false })) - let head_string = match heads.first() { Present { value: h } => h.id as String Absent => "" } - let expected_revision = issue_form_field(body: request.body, name: "expected_revision") - if expected_revision != "" && expected_revision != head_string { - serve_text_response(status: 409, content_type: text_plain_utf8, body: join(["comment refused: the issue changed since the page rendered (expected revision ", expected_revision, ", head ", head_string, ") — reload and comment again\n"], "")) - } else { - match author_principal { - Absent => - serve_text_response(status: 403, content_type: text_plain_utf8, body: "comment refused: admitted write carried no author principal (unreachable past the write law)\n") - Present { value: author_ref } => - let author_identity = EventPrincipalAuthenticated { ref: author_ref } - let key_field = trim(issue_form_field(body: request.body, name: "key")) - let comments = roadmap_issue_comments(envs: envelopes) - if key_field != "" && roadmap_comment_created_key_present(envs: envelopes, comment: key_field as NonEmptyStr) { - serve_comment_recorded_response(node_id: node_id, comments: comments, comment: key_field, event_id: "", already_present: true) - } else if key_field == "" && roadmap_comment_retry_present(envs: envelopes, author: author_identity, body: body as NonEmptyStr, head: match heads.first() { Present { value: h } => Present { value: h.id } Absent => none }) { - serve_comment_recorded_response(node_id: node_id, comments: comments, comment: "", event_id: "", already_present: true) - } else { - let at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } - let comment_id = if key_field == "" { join([principal_ref_label(p: author_ref), ":", body], "") as NonEmptyStr } else { key_field as NonEmptyStr } - match roadmap_event_append(layout: layout, event: RoadmapEvent { node: nid, author: author_identity, parent: match heads.first() { Present { value: h } => Present { value: h.id } Absent => none }, kind: CommentCreated { comment: comment_id, body: body as NonEmptyStr, visibility: "public" as NonEmptyStr, parent_comment: none }, recorded_at: at }) { - EventAppended { id } => - serve_comment_recorded_response(node_id: node_id, comments: [IssueComment { ordinal: count(comments) + 1, comment: comment_id, author: author_identity, body: "", visibility: "public" as NonEmptyStr, parent_comment: none, created_at: "", edited_at: "", deleted: false }], comment: comment_id as String, event_id: id as String, already_present: false) - EventAlreadyPresent { id } => - serve_comment_recorded_response(node_id: node_id, comments: comments, comment: comment_id as String, event_id: id as String, already_present: true) - EventAppendRefused { id: _, step, reason } => - serve_text_response(status: 503, content_type: text_plain_utf8, body: join(["comment refused at ", step, ": ", reason, "\n"], "")) - } - } - } - } + let observed_at = match clock_now_probed_at() { Present { value: t } => t as String Absent => "" } + if !issue_detail_node_exists(proj: roadmap_authority_projection(), node_id: node_id) { serve_text_response(status: 404, content_type: text_plain_utf8, body: "issue not found") } + else { match human_write_admission(route_path: concat("/issue/", concat(node_id, "/comment")) as NonEmptyStr, + action: ServeActionCommentIssue, security: request.security, authorization_policy: tracker_write_authorization_policy, + deployment_policy: roadmap_deployment_access_policy, csrf_presented: issue_form_field(body: request.body, name: "csrf"), observed_at: observed_at) { + Deny { refusal } => serve_text_response(status: 403, content_type: text_plain_utf8, body: access_refusal_reason(refusal: refusal)) + Permit => if !issue_command_form_valid(body: request.body) { serve_text_response(status: 400, content_type: text_plain_utf8, body: "comment refused: a valid operation key and explicit expected_revision are required") } + else { match authenticated_write_author_principal(security: request.security) { + Absent => serve_text_response(status: 403, content_type: text_plain_utf8, body: "authenticated principal required") + Present { value: author } => { + let body = issue_comment_body_of(wire: request.body) + if body == "" { serve_text_response(status: 400, content_type: text_plain_utf8, body: "comment refused: empty body") } + else { + let layout = roadmap_event_carrier_layout_for_instance(instance: instance) + let expected = issue_form_field(body: request.body, name: "expected_revision") + let operation = issue_form_field(body: request.body, name: "key") + let outcome = issue_command_apply(layout: layout, command: CreateComment { subject: IssueCommandSubject { + node: node_id as NonEmptyStr as RoadmapNodeId, author: EventPrincipalAuthenticated { ref: author }, operation: operation as NonEmptyStr, + expected: if expected == "" { none } else { Present { value: expected as NonEmptyStr as RoadmapEventId } } }, body: body as NonEmptyStr }, recorded_at: observed_at) + match outcome { + EventAppendRefused { id: _, step, reason } => issue_command_refusal_response(step: step, reason: reason) + _ => match roadmap_events_read(layout: layout, node: node_id as NonEmptyStr as RoadmapNodeId) { + EventsReadRefused { node: _, step, reason } => serve_text_response(status: 503, content_type: text_plain_utf8, + body: concat("comment commit acknowledged, but readback refused; retry with the same operation key: ", concat(step, concat(": ", reason)))) + EventsRead { node: _, envelopes } => serve_comment_recorded_response(node_id: node_id, comments: roadmap_issue_comments(envs: envelopes), comment: operation, + event_id: match outcome { EventAppended { id } => id as String EventAlreadyPresent { id } => id as String _ => "" }, + already_present: match outcome { EventAlreadyPresent { id: _ } => true _ => false }) } + } } } - } - } + } } + } } } // THE COMMENT RESPONSE, factored pure over the recorded comments so the witness drives it without @@ -1232,23 +1168,9 @@ fn serve_comment_recorded_response(node_id: String, comments: List } } -// THE COMMENT BODY'S WIRE SHAPE: the form orders key first, body LAST, so the body may carry any -// character including newlines. The body is everything after the FIRST body= field opener — -// occurrences inside the markdown stay verbatim because the cut is at the first, never the last. +// The body is a decoded value; its newlines and field-looking text are never reparsed. fn issue_comment_body_of(wire: String) -> String { - if starts_with(s: wire, prefix: "body=") { - substring(s: wire, start: 5, end: string_length(s: wire)) - } else { - let parts = split(s: wire, delimiter: "\nbody=") - if count(parts) == 1 { - "" - } else { - match parts.first() { - Present { value: before } => substring(s: wire, start: string_length(s: before) + 6, end: string_length(s: wire)) - Absent => "" - } - } - } + issue_form_field(body: wire, name: "body") } fn serve_dispatch_handler_response(params: List) -> ServeHttpResponse { diff --git a/dag/gunbc/roadmap/roadmap_supervisor.dag b/dag/gunbc/roadmap/roadmap_supervisor.dag index a0bff095f33..849accc81ac 100644 --- a/dag/gunbc/roadmap/roadmap_supervisor.dag +++ b/dag/gunbc/roadmap/roadmap_supervisor.dag @@ -1,8 +1,8 @@ module gunbc.roadmap.roadmap_supervisor import std.types { String, Int, List, NonEmptyStr } -import gunbc.roadmap_model { RoadmapEdge } -import gunbc.roadmap.roadmap_project { node_project_membership, project_member_ids, MembershipStandalone, MembershipMember, MembershipAmbiguous } +import gunbc.roadmap_nesting { RoadmapMembership } +import gunbc.roadmap.roadmap_project { node_project_membership, project_member_ids, MembershipStandalone, MembershipMember, MembershipAmbiguous, project_root_for_node } import extdeps.languages.json.emit { JsonValue, json_string, @@ -33,7 +33,7 @@ type SupervisorVerdict | VerdictStuck { cause: String } // THE TARGET IS DERIVED FROM THE DECOMPOSITION DAG, NEVER ROSTERED. A leaf escalates to the -// supervisor of the arc that depends on it (its project, per gunbc.roadmap.roadmap_project); a +// supervisor of its authored parent (its project, per gunbc.roadmap.roadmap_project); a // project-of-one leaf has no arc above it, so its own node's supervisor convenes; a root arc -- // standalone with members of its own -- has no supervisor but the top. Ambiguous membership has no // derivation at all: the authority must say which arc owns the delivery before anyone can @@ -42,17 +42,19 @@ type SupervisorTarget = SupervisorForNode { node_id: String } | SupervisorAtTop -fn supervisor_target_for(node_id: String, edges: List) -> SupervisorTarget? { - match node_project_membership(node_id: node_id, edges: edges) { +fn supervisor_target_for(node_id: String, memberships: List) -> SupervisorTarget? { + if project_root_for_node(node_id: node_id, memberships: memberships) == none { none } else { + match node_project_membership(node_id: node_id, memberships: memberships) { MembershipMember { arc } => Present { value: SupervisorForNode { node_id: arc } } MembershipStandalone => - if count(project_member_ids(arc: node_id, edges: edges)) > 0 { + if count(project_member_ids(arc: node_id, memberships: memberships)) > 0 { Present { value: SupervisorAtTop } } else { Present { value: SupervisorForNode { node_id: node_id } } } MembershipAmbiguous { arcs: _ } => none } + } } fn supervisor_target_line(target: SupervisorTarget) -> String { diff --git a/dag/test/claim/auth/google_workspace_admin_convergence_witness_test.dag b/dag/test/claim/auth/google_workspace_admin_convergence_witness_test.dag new file mode 100644 index 00000000000..3b36a1fe9d8 --- /dev/null +++ b/dag/test/claim/auth/google_workspace_admin_convergence_witness_test.dag @@ -0,0 +1,67 @@ +module test.claim.auth.google_workspace_admin_convergence_witness_test + +import std.types { Bool, String, NonEmptyStr } +import std.goal_assessment { GoalSatisfied, GoalAssessmentRefused, GoalDiverged, GoalObservationRefused } +import gunbc.principal_projection { PrincipalRef } +import extdeps.google.workspace { + ClientAccess, AppSpecificGoogleData, + WorkspacePhotoEditing, PhotoEditing, ExternalDirectorySharing, OAuthClientRegistration, + workspace_admin_read_surface, workspace_admin_write_surface, PolicyApiReadSupported, ConsoleReadbackRequired, + ManualConsoleAdministration, WorkspaceDirectorySharing, DirectorySharing, RequesterBasicProfileOnly, +} +import gunbc.auth.google_workspace_admin_convergence { + workspace_admin_values_equal, + WorkspaceIdentityBinding, workspace_identity_admin_obligations, BasicSignInOnly, + WorkspaceAdminObligation, WorkspaceAdminReadback, AdminConsoleReadback, WorkspacePolicyApiReadback, + EffectiveAtDeclaredScope, EffectiveWithUnresolvedOverrides, workspace_admin_assess, workspace_admin_inspect, WorkspaceAdminNotObserved, +} + +fn fixture_admin() -> PrincipalRef { PrincipalRef { authority: "google-oidc", namespace: "accounts", subject: "fixture-subject" } } +fn fixture_photo_goal() -> WorkspaceAdminObligation { + WorkspaceAdminObligation { subject: WorkspacePhotoEditing { customer: "customer-A", org_unit: "/" }, desired: PhotoEditing { allowed: true }, desired_revision: "policy-r1", rationale: "fixture" } +} +fn fixture_photo_read(allowed: Bool = true, other_tenant: Bool = false) -> WorkspaceAdminReadback { + WorkspaceAdminReadback { subject: WorkspacePhotoEditing { customer: if other_tenant { "customer-B" } else { "customer-A" }, org_unit: "/" }, observed: PhotoEditing { allowed: allowed }, + source: AdminConsoleReadback { administrator: fixture_admin(), evidence: "receipt://fixture" }, observed_at_epoch: 100, valid_until_epoch: 200, coverage: EffectiveAtDeclaredScope } +} + +test fn matching_readback_is_not_login_success_or_an_unobserved_setting() -> Bool { + match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(), admitted_admins: [fixture_admin()], now_epoch: 150) { GoalSatisfied { evidence } => evidence.desired_revision == "policy-r1" _ => false } + && match workspace_admin_inspect(goal: fixture_photo_goal(), observation: WorkspaceAdminNotObserved { reason: "no readback" }, admitted_admins: [fixture_admin()], now_epoch: 150) { GoalObservationRefused { subject: _, goal: _, request: _, cause: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(allowed: false), admitted_admins: [fixture_admin()], now_epoch: 150) { GoalDiverged { deviations: _ } => true _ => false } +} + +test fn wrong_tenant_expiry_and_unapproved_reader_refuse() -> Bool { + match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(other_tenant: true), admitted_admins: [fixture_admin()], now_epoch: 150) { GoalAssessmentRefused { cause: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(), admitted_admins: [fixture_admin()], now_epoch: 200) { GoalAssessmentRefused { cause: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(), admitted_admins: [], now_epoch: 150) { GoalAssessmentRefused { cause: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_photo_goal(), receipt: fixture_photo_read(), admitted_admins: [fixture_admin()], now_epoch: 99) { GoalAssessmentRefused { cause: _ } => true _ => false } +} + +test fn supported_policy_read_is_distinct_from_manual_mutation() -> Bool { + let binding = WorkspaceIdentityBinding { customer: "customer-A", project: "project-A", client: "client-A", top_org_unit: "/", redirect_uri: "https://fixture.invalid/callback", desired_revision: "r1" } + count(workspace_identity_admin_obligations(binding: binding, discovery: BasicSignInOnly)) == 5 + && match workspace_admin_read_surface(control: ExternalDirectorySharing) { PolicyApiReadSupported { setting_type, documentation: _ } => setting_type == "directory.external_directory_sharing" _ => false } + && match workspace_admin_write_surface(control: ExternalDirectorySharing) { ManualConsoleAdministration { documentation: _ } => true } + && match workspace_admin_read_surface(control: OAuthClientRegistration) { ConsoleReadbackRequired { documentation: _ } => true _ => false } +} + +fn fixture_directory_goal() -> WorkspaceAdminObligation { + WorkspaceAdminObligation { subject: WorkspaceDirectorySharing { customer: "customer-A" }, desired: DirectorySharing { policy: RequesterBasicProfileOnly }, desired_revision: "r1", rationale: "basic sign-in" } +} +fn fixture_directory_read(setting: String = "directory.external_directory_sharing", unresolved: Bool = false) -> WorkspaceAdminReadback { + WorkspaceAdminReadback { subject: WorkspaceDirectorySharing { customer: "customer-A" }, observed: DirectorySharing { policy: RequesterBasicProfileOnly }, + source: WorkspacePolicyApiReadback { policy_resource: "policies/fixture", setting_type: setting as NonEmptyStr, evidence: "receipt://fixture-policy" }, + observed_at_epoch: 100, valid_until_epoch: 200, coverage: if unresolved { EffectiveWithUnresolvedOverrides { reason: "child OU unread" } } else { EffectiveAtDeclaredScope } } +} + +test fn policy_readback_requires_matching_setting_and_resolved_effective_coverage() -> Bool { + match workspace_admin_assess(goal: fixture_directory_goal(), receipt: fixture_directory_read(), admitted_admins: [], now_epoch: 150) { GoalSatisfied { evidence: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_directory_goal(), receipt: fixture_directory_read(setting: "unrelated.setting"), admitted_admins: [], now_epoch: 150) { GoalAssessmentRefused { cause: _ } => true _ => false } + && match workspace_admin_assess(goal: fixture_directory_goal(), receipt: fixture_directory_read(unresolved: true), admitted_admins: [], now_epoch: 150) { GoalAssessmentRefused { cause: _ } => true _ => false } +} + +test fn scope_order_is_not_policy_drift_but_extra_privilege_is() -> Bool { + workspace_admin_values_equal(a: ClientAccess { access: AppSpecificGoogleData { scopes: ["openid", "email", "profile"] } }, b: ClientAccess { access: AppSpecificGoogleData { scopes: ["profile", "openid", "email"] } }) + && !workspace_admin_values_equal(a: ClientAccess { access: AppSpecificGoogleData { scopes: ["openid", "email", "profile"] } }, b: ClientAccess { access: AppSpecificGoogleData { scopes: ["openid", "email", "profile", "drive"] } }) +} diff --git a/dag/test/claim/extdeps/issue_tracker_witness_test.dag b/dag/test/claim/extdeps/issue_tracker_witness_test.dag index dddb89aa504..74d26823e9a 100644 --- a/dag/test/claim/extdeps/issue_tracker_witness_test.dag +++ b/dag/test/claim/extdeps/issue_tracker_witness_test.dag @@ -2,7 +2,7 @@ module test.claim.extdeps.issue_tracker_witness import extdeps.google.issue_tracker { IssueActionAssign, IssueActionCreate, IssueActionMarkDuplicate, IssueActionStartWork, - Component, fleet_component_hosts, fleet_components, browse_component_tree, + Component, browse_component_tree, IssueActionEnabled, IssueActionDisabled, IssueButtonContainedPrimary, IssueButtonText, issue_action_availability_wire, @@ -658,33 +658,6 @@ test fn w_assignee_results_display_is_plain_text_with_verified_placeholder() -> && (issue_assignee_unassigned_display as String) == "--" } -test fn w_fleet_component_tree_hosts_and_arms() -> Bool { - count(fleet_components()) == 20 - && count(fleet_component_hosts) == 12 - && (match (filter(fleet_components(), c => c.path == ["srv11", "serving", "group-b"])).first() { - Present { value: c } => c.id == 111 - Absent => false - }) - && count(filter(fleet_components(), c => count(c.path) == 3)) == 8 -} - -test fn w_browse_components_derives_the_tree() -> Bool { - let tree = browse_component_tree(components: fleet_components()) - count(tree) == 20 - && (match tree.first() { - Present { value: e } => e.component.path == ["srv1"] && e.depth == 1 - Absent => false - }) - && (match tree.skip(n: 12).first() { - Present { value: e } => e.component.path == ["srv5", "serving", "group-a"] && e.depth == 3 - Absent => false - }) - && (match tree.skip(n: 19).first() { - Present { value: e } => e.component.path == ["srv12", "serving", "group-b"] - Absent => false - }) -} - // THE RESULTS-TABLE ROSTER (owner ruling 2026-09-24, the derivation law): the column set, the // header spellings, the row anatomy, and the grid template are DECLARED in this model and every // surface derives from them. This pin states the served roster IS the capture's amended by the diff --git a/dag/test/claim/http/form_urlencoded_witness_test.dag b/dag/test/claim/http/form_urlencoded_witness_test.dag new file mode 100644 index 00000000000..eae1676be0e --- /dev/null +++ b/dag/test/claim/http/form_urlencoded_witness_test.dag @@ -0,0 +1,26 @@ +module test.claim.http.form_urlencoded_witness_test + +import std.types { Bool } +import extdeps.http.form_urlencoded { form_decode, form_field, FormRefused } + +test fn browser_encoded_multiline_is_one_field() -> Bool { + let decoded = form_decode(wire: "csrf=token&expected_revision=&body=first%0D%0Acsrf%3Dforged%0D%0A%2B+%26%3D+%E2%98%83") + form_field(form: decoded, name: "csrf") == Present { value: "token" } + && form_field(form: decoded, name: "expected_revision") == Present { value: "" } + && form_field(form: decoded, name: "missing") == none + && form_field(form: decoded, name: "body") == Present { value: "first\r\ncsrf=forged\r\n+ &= ☃" } +} + +test fn form_decodes_once_and_refuses_ambiguity() -> Bool { + form_field(form: form_decode(wire: "body=%2526body%253Dsecond"), name: "body") == Present { value: "%26body%3Dsecond" } + && match form_decode(wire: "csrf=a&%63srf=b") { FormRefused { reason: _ } => true _ => false } + && match form_decode(wire: "body=%ZZ") { FormRefused { reason: _ } => true _ => false } + && match form_decode(wire: "body=%FF") { FormRefused { reason: _ } => true _ => false } +} + +fn browser_post_control(wire: String) -> Bool { + let decoded = form_decode(wire: wire) + form_field(form: decoded, name: "csrf") == Present { value: "real-token" } + && form_field(form: decoded, name: "expected_revision") == Present { value: "" } + && form_field(form: decoded, name: "body") == Present { value: "first\r\ncsrf=forged\r\n☃ + & =" } +} diff --git a/dag/test/claim/long/roadmap_page_witness_test.dag b/dag/test/claim/long/roadmap_page_witness_test.dag index 0a472db91ac..bf761f836d7 100644 --- a/dag/test/claim/long/roadmap_page_witness_test.dag +++ b/dag/test/claim/long/roadmap_page_witness_test.dag @@ -1,5 +1,8 @@ module test.claim.long.roadmap_page_witness +import gunbc.roadmap_spawner { doc_all_edges } +import gunbc.roadmap_nesting { declared_roadmap_memberships } + import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import gunbc.harness.harness_throughput { HarnessRoundLedger, RoundsUnobserved, RoundsObserved, RoundObservation, HarnessThroughput, ThroughputUnobserved } @@ -674,6 +677,7 @@ fn parity_specimen_view(activity: gunbc.roadmap_presentation.ActivityView) -> gu disclosure: gunbc.roadmap_presentation.NoDisclosure, rounds: RoundsUnobserved { reason: "fixture models no provider event stream" as NonEmptyStr }, worker_alive: false, + assignment: none, standing: none, attempt_stale: false, } @@ -775,6 +779,7 @@ test fn witness_stale_attempt_row_renders_dimmed() -> Bool { disclosure: gunbc.roadmap_presentation.NoDisclosure, rounds: RoundsUnobserved { reason: "fixture models no provider event stream" as NonEmptyStr }, worker_alive: false, + assignment: none, standing: none, attempt_stale: true, } @@ -885,7 +890,7 @@ fn fx_page_project_doc() -> RoadmapDocument { } fn fx_page_project_html() -> HtmlEmitResult { - try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: fx_page_project_doc(), merged: [], launch: dispatch_doc_standing())) + try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: fx_page_project_doc(), merged: [], launch: dispatch_doc_standing(), memberships: fixture_nesting_relation(edges: doc_all_edges(doc: fx_page_project_doc())))) } fn occurrence_count(s: String, pattern: String) -> Int { @@ -998,7 +1003,7 @@ fn fx_page_chain_doc() -> RoadmapDocument { } fn fx_page_chain_html() -> HtmlEmitResult { - try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: fx_page_chain_doc(), merged: [], launch: dispatch_doc_standing())) + try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: fx_page_chain_doc(), merged: [], launch: dispatch_doc_standing(), memberships: fixture_nesting_relation(edges: doc_all_edges(doc: fx_page_chain_doc())))) } fn fx_chain_top_project_view() -> gunbc.roadmap_presentation.ProjectTreeView { @@ -1015,7 +1020,7 @@ fn fx_chain_top_project_view() -> gunbc.roadmap_presentation.ProjectTreeView { Absent => fx_project_tree_view(auto_open: false) Present { value: top } => gunbc.roadmap_presentation.project_tree_view( - row: top, rows: rows, edges: edges, members: [], depth: 1, fuel: count(rows)) + row: top, rows: rows, edges: edges, members: [], depth: 1, fuel: count(rows), memberships: fixture_nesting_relation(edges: edges)) } } @@ -1072,7 +1077,7 @@ test fn witness_shell_arc_renders_as_project_with_ten_tasks() -> Bool { ] let chain_ids = ["shell-gate-migration", "shell-effectplan-to-bash", "shell-typed-invocation"] match try_serialize_html_source(node: roadmap_daily_workspace_page( - doc: gunbc.roadmap_authority.roadmap_authority_for(focus: NoFocus), merged: [])) { + doc: gunbc.roadmap_authority.roadmap_authority_for(focus: NoFocus), merged: [], memberships: fixture_nesting_relation(edges: doc_all_edges(doc: gunbc.roadmap_authority.roadmap_authority_for(focus: NoFocus))))) { EmitRejected { reason: _ } => false EmitOk { html: h } => let arc_row = li_of_node(html: h, node_id: "shell-residue-zero") @@ -1112,7 +1117,7 @@ test fn witness_collapsed_chain_renders_one_row_with_chain_line() -> Bool { elements: [ section_group(label: "", nodes: [top, mid, t1, t2], edges: edges) ], } ], } - match try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: doc, merged: [], launch: dispatch_doc_standing())) { + match try_serialize_html_source(node: roadmap_daily_workspace_page_launch(doc: doc, merged: [], launch: dispatch_doc_standing(), memberships: fixture_nesting_relation(edges: doc_all_edges(doc: doc)))) { EmitRejected { reason: _ } => false EmitOk { html: h } => let top_row = li_of_node(html: h, node_id: "page-collapse-top") @@ -1176,6 +1181,7 @@ fn hero_row_view(node_id: String, activity: gunbc.roadmap_presentation.ActivityV disclosure: gunbc.roadmap_presentation.NoDisclosure, rounds: hero_rounds_fixture(), worker_alive: alive, + assignment: none, standing: none, attempt_stale: false, } @@ -1435,7 +1441,7 @@ fn fx_shell_live_workspace_html(refused: Bool) -> HtmlEmitResult { let rows = fx_shell_live_rows(refused: refused) let ws = gunbc.roadmap_presentation.daily_workspace_view( rows: rows, edges: edges, program: none, - observation: gunbc.roadmap_presentation.ObservationsNominal, families: [], members: []) + observation: gunbc.roadmap_presentation.ObservationsNominal, families: [], members: [], memberships: declared_roadmap_memberships()) try_serialize_html_source(node: gunbc.roadmap_page.workspace_markup(ws: ws, edges: edges)) } @@ -1471,12 +1477,12 @@ test fn witness_shell_project_row_heads_the_table_with_live_task() -> Bool { match fx_shell_live_workspace_html(refused: true) { EmitRejected { reason: _ } => false EmitOk { html: refused_h } => - let project_row = li_of_node(html: live, node_id: "shell-residue-zero") + let project_row = li_of_node(html: live, node_id: "shell-typed-invocation") let live_row = li_of_node(html: live, node_id: "shell-dag-cron-entry-line-builder") let refused_row = li_of_node(html: refused_h, node_id: "shell-dag-cron-entry-line-builder") !string_contains(s: project_row, pattern: "node-project") && !string_contains(s: project_row, pattern: " tasks closed") - && marker_before(s: live, first: "data-node=\"shell-residue-zero\"", second: "data-node=\"shell-dag-cron-entry-line-builder\"") + && marker_before(s: live, first: "data-node=\"shell-typed-invocation\"", second: "data-node=\"shell-dag-cron-entry-line-builder\"") && all(fx_shell_task_ids(), id => occurrence_count(s: live, pattern: join(["data-node=\"", id, "\" tabindex=\"0\""], "")) == 1) && string_contains(s: live_row, pattern: "data-band=\"active\"") && !string_contains(s: live_row, pattern: "activity-line-error-refusal") @@ -1556,6 +1562,7 @@ fn issue_detail_fixture_view() -> gunbc.roadmap_presentation.WorkRowView { disclosure: gunbc.roadmap_presentation.NoDisclosure, rounds: hero_rounds_fixture(), worker_alive: true, + assignment: none, standing: Present { value: NodeClaimed { by: fixture_human_principal(), since: "2026-09-23" } }, attempt_stale: false, } @@ -1574,7 +1581,8 @@ fn issue_detail_fixture_panel() -> gunbc.roadmap_presentation.IssueLinkPanel { gunbc.roadmap_presentation.issue_link_panel( rn: gunbc.roadmap_authority.authored(identity: "rnfx_idspec", id: "issue-detail-specimen", done: false, content: "Specimen issue"), nodes: nodes, - edges: edges) + edges: edges, + memberships: [gunbc.roadmap_nesting.RoadmapMembership { member: "issue-detail-specimen", project: "issue-detail-parent" }, gunbc.roadmap_nesting.RoadmapMembership { member: "issue-detail-child", project: "issue-detail-specimen" }]) } fn fixture_human_ref() -> gunbc.principal_projection.PrincipalRef { @@ -1615,7 +1623,7 @@ test fn witness_issue_detail_page_renders_header_fields_and_tabs() -> Bool { string_contains(s: h, pattern: "
") && string_contains(s: h, pattern: "issue-detail-specimen") && string_contains(s: h, pattern: "

Specimen issue

") - && string_contains(s: h, pattern: "Task") + && !string_contains(s: h, pattern: "") && occurrence_count(s: h, pattern: "issue-status\"") == 0 && string_contains(s: h, pattern: " + + + ''') + page.evaluate('''() => { + window.gunbcSessionRead = Promise.resolve({authenticated: true}); + window.posts = []; window.contexts = 0; window.loseResponse = true; + window.fetch = async (path, init) => { + if (!init.method) return {ok: true, json: async () => ({key: 'new-' + (++contexts), csrf: 'fixture-csrf', expected_revision: 'new-head', options: [{value: 'person@example.invalid', label: 'Person'}, {value: 'other@example.invalid', label: 'Other'}]})}; + posts.push(Object.fromEntries(new URLSearchParams(init.body))); + if (loseResponse) { loseResponse = false; throw new Error('fixture lost response'); } + return {ok: true, text: async () => 'assigned: fixture'}; + }; + }''') + page.add_script_tag(content=script) + page.locator('form button').click() + page.wait_for_function('posts.length === 1 && !document.querySelector("form button").disabled') + page.locator('form button').click() + page.wait_for_function('posts.length === 2 && !document.querySelector("form button").disabled') + posts = page.evaluate('posts') + assert posts[0] == posts[1], posts + assert posts[0]['key'] == 'native-stable-key' and posts[0]['expected_revision'] == 'original-head' + page.evaluate('loseResponse = true') + page.locator('.issue-inline-assign').click() + page.locator('select').select_option('person@example.invalid') + page.wait_for_function('posts.length === 3 && !document.querySelector(".issue-inline-assign").disabled') + page.locator('.issue-inline-assign').click() + assert page.locator('select option[value="other@example.invalid"]').is_disabled() + page.locator('select').select_option('person@example.invalid') + page.wait_for_function('posts.length === 4 && !document.querySelector(".issue-inline-assign").disabled') + posts = page.evaluate('posts') + assert posts[2] == posts[3], posts + assert page.locator('.issue-inline-assign').inner_text() == 'Person' + browser.close() +print('PASS emitted client: native-form key retained and inline lost-response retry reuses exact command') diff --git a/tools/tests/issue_command_wet.py b/tools/tests/issue_command_wet.py new file mode 100644 index 00000000000..aa483d7ff4b --- /dev/null +++ b/tools/tests/issue_command_wet.py @@ -0,0 +1,54 @@ +"""Run actual .dag commands against a disposable Git remote, including a forced CAS race.""" +import concurrent.futures +import json +import subprocess +import tempfile +from pathlib import Path + +repo = Path(__file__).resolve().parents[2] +subprocess.run(['python3', str(repo/'tools/tests/dag_validation_closure.py'), 'test.manual.issue_command_wet', '--output', 'target/issue-command-wet'], cwd=repo, check=True) +root = Path(tempfile.mkdtemp(prefix='gunbc-issue-command-')) +fixture = root/'repo' +def git(*args, cwd=None): + return subprocess.run(['/usr/bin/git', *args], cwd=cwd, check=True, capture_output=True, text=True).stdout + +git('init', '--bare', str(root/'remote.git')) +git('clone', str(root/'remote.git'), str(fixture)) +git('config', 'user.name', 'Issue Fixture', cwd=fixture) +git('config', 'user.email', 'fixture@example.invalid', cwd=fixture) +git('commit', '--allow-empty', '-m', 'fixture root', cwd=fixture) +git('push', 'origin', 'HEAD', cwd=fixture) +(fixture/'instance').mkdir() +base = ['systemd-run', '--user', '--scope', '-p', 'MemoryMax=6G', '-p', 'MemorySwapMax=0', '--quiet', + str(repo/'target/release/gunbc'), 'run', '--source-root', 'target/issue-command-wet', + '--entry', 'target/issue-command-wet/test.manual.issue_command_wet.dag', '--function', 'issue_command_wet', '--arg', 'repo='+str(fixture)] +with (root/'sequential.log').open('w') as out: + subprocess.run(base, cwd=repo, stdout=out, stderr=subprocess.STDOUT, check=True) +barrier = root/'barrier' +barrier.mkdir() +wrapper = root/'git-race' +wrapper.write_text('''#!/usr/bin/python3 +import os,sys,time +from pathlib import Path +barrier=Path(__file__).parent/'barrier' +if sys.argv[1:2] == ['push'] and any(x.startswith('--force-with-lease=') for x in sys.argv): + (barrier/str(os.getpid())).touch() + deadline=time.monotonic()+300 + while len(list(barrier.iterdir())) < 2: + if time.monotonic() > deadline: sys.exit(91) + time.sleep(.05) +os.execv('/usr/bin/git',['git',*sys.argv[1:]]) +''') +wrapper.chmod(0o700) +def race(name): + with (root/(name+'.log')).open('w') as out: + return subprocess.run(base+['--arg', 'mode='+name, '--arg', 'git_program='+str(wrapper)], cwd=repo, stdout=out, stderr=subprocess.STDOUT).returncode +with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool: + results = list(pool.map(race, ['race-a', 'race-b'])) +assert sorted(results) == [0,1], (root, results) +files = git('--git-dir='+str(root/'remote.git'), 'ls-tree', '-r', '--name-only', 'roadmap-events', 'events/raced-issue/').splitlines() +assert len(files) == 1, (root, files) +receipts = git('--git-dir='+str(root/'remote.git'), 'ls-tree', '-r', '--name-only', 'roadmap-events', 'operations/raced-issue/').splitlines() +assert len(receipts) == 1, (root, receipts) +(root/'receipt.json').write_text(json.dumps({'sequential': 'passed', 'race_exit_codes': results, 'events': files, 'operations': receipts}, indent=2)) +print('PASS actual command CAS, replay, stale writer, different payload, causal readback, concurrent publication:', root) diff --git a/tools/tests/issue_form_browser_control.py b/tools/tests/issue_form_browser_control.py new file mode 100644 index 00000000000..8d34e43d7c0 --- /dev/null +++ b/tools/tests/issue_form_browser_control.py @@ -0,0 +1,35 @@ +"""Capture a native Chromium POST and feed its wire bytes to the actual .dag decoder. +Run with the repository's Playwright Python environment; no live site or credentials. +""" +import os +import subprocess +from pathlib import Path +from playwright.sync_api import sync_playwright + +repo = Path(__file__).resolve().parents[2] +with sync_playwright() as p: + options = {} + if os.environ.get('CHROMIUM_EXECUTABLE'): + options['executable_path'] = os.environ['CHROMIUM_EXECUTABLE'] + browser = p.chromium.launch(**options) + page = browser.new_page() + captured = [] + def route(request): + if request.request.method == 'POST': + captured.append(request.request.post_data) + assert request.request.headers['content-type'].startswith('application/x-www-form-urlencoded') + request.fulfill(status=200, body='recorded fixture') + else: + request.fulfill(status=200, content_type='text/html', body='''
''') + page.route('http://issue-fixture.test/**', route) + page.goto('http://issue-fixture.test/') + page.locator('textarea').fill('first\ncsrf=forged\n☃ + & =') + with page.expect_navigation(): + page.locator('button').click() + browser.close() +assert len(captured) == 1 +subprocess.run(['systemd-run', '--user', '--scope', '-p', 'MemoryMax=6G', '-p', 'MemorySwapMax=0', '--quiet', + str(repo/'target/release/gunbc'), 'run', '--source-root', 'target/form-codec-validation', + '--entry', 'target/form-codec-validation/test.claim.http.form_urlencoded_witness_test.dag', + '--function', 'browser_post_control', '--arg', 'wire='+captured[0], '--claim-run'], cwd=repo, check=True) +print('PASS native Chromium POST through .dag form decoder') diff --git a/tools/tests/issue_sort_browser_control.py b/tools/tests/issue_sort_browser_control.py new file mode 100644 index 00000000000..ba65d5eadaa --- /dev/null +++ b/tools/tests/issue_sort_browser_control.py @@ -0,0 +1,63 @@ +"""Run emitted DAG sort/focus behavior in Chromium; progress values are explicit fixtures.""" +import os +from pathlib import Path +from playwright.sync_api import sync_playwright +script = Path('/tmp/issue-sort-script.js').read_text() +project = 'shell-typed-invocation' +names = ['shell-dag-cron-entry-line-builder', 'shell-dag-floor-peak-calibration-rows', 'shell-dag-ci-fmt-gate-line', 'shell-dag-ci-deploy-invoke-prelude', 'shell-dag-ci-heal-regen-invoke', 'shell-dag-compile-pool-slice-install'] +def row(node, dependency='', complete=False, owner=project): + return f'
  • {node}
  • ' +fixture = '
      ' +fixture += row(names[5], names[4]) + row('unrelated', owner='unrelated') +fixture += ''.join(row(name, names[i-1] if i else '', complete=True) for i,name in enumerate(names[:5])) +fixture += row(project, names[5]) + '
    ' +with sync_playwright() as p: + browser = p.chromium.launch(executable_path=os.environ['CHROMIUM_EXECUTABLE'], headless=True) + page = browser.new_page(viewport={'width': 1100, 'height': 850}) + page.set_content(fixture) + style = Path('/tmp/issue-sort-style.css') + if style.exists(): page.add_style_tag(content=style.read_text()) + errors = [] + page.on('pageerror', lambda err: errors.append(str(err))) + page.add_script_tag(content=script) + order = lambda: page.locator('li').evaluate_all('(rows) => rows.map(r => r.dataset.node)') + visible = lambda: page.locator('.issue-title-link:visible').evaluate_all('(links) => links.map(a => a.closest("li").dataset.node)') + original = order() + page.locator(f'[data-node="{names[5]}"] a').click() + assert visible() == names[2:] + [project], visible() + assert page.locator('.issue-focus-controls button').nth(1).inner_text() == '… 2 earlier' + assert page.locator('.issue-focus-controls a').get_attribute('href') == '/issue/' + names[5] + page.locator('.issue-focus-controls button').nth(1).click() + assert visible() == names + [project] + page.locator('.issue-sort-undo').click() + assert visible() == names[2:] + [project] + page.locator('[data-sort-key]').click() + assert page.locator('.issue-topological').get_attribute('aria-pressed') == 'false' + assert len(visible()) == 8 + page.locator('.issue-sort-undo').click() + assert visible() == names[2:] + [project] + page.locator('.issue-search').fill('compile-pool') + assert visible() == [names[5]] + page.locator('.issue-search').fill('') + page.locator('.issue-focus-controls button').first.click() + assert order() == original and len(visible()) == 8 + page.locator(f'[data-node="{names[5]}"] a').focus() + page.keyboard.press('Enter') + assert visible() == names[2:] + [project] + page.locator('.issue-focus-controls button').first.click() + page.locator(f'[data-node="{names[0]}"]').evaluate('(row, dependency) => row.dataset.dependencies = dependency', names[5]) + before = order() + page.locator('.issue-topological').click() + assert order() == before and 'cyclic' in page.locator('.issue-sort-status').inner_text() + page.locator(f'[data-node="{names[0]}"]').evaluate('(row) => row.dataset.dependencies = ""') + page.locator('[data-node="unrelated"]').evaluate('(row) => row.dataset.dependencies = "unrelated"') + page.locator(f'[data-node="{names[5]}"] a').click() + assert visible() == names[2:] + [project] + page.locator('.issue-topological').click() + assert page.locator('.issue-topological').get_attribute('aria-pressed') == 'false' + assert len(visible()) == 8 + page.locator('.issue-sort-undo').click() + assert visible() == names[2:] + [project] + assert not errors, errors + browser.close() +print('PASS emitted focus/sort: recursive project identity, recent three, expansion, upcoming, undo, search, keyboard, cycle refusal')