diff --git a/dag/gunbc/auth/profile_projection.dag b/dag/gunbc/auth/profile_projection.dag index 6cf9c5ff2a9..59a67398e15 100644 --- a/dag/gunbc/auth/profile_projection.dag +++ b/dag/gunbc/auth/profile_projection.dag @@ -327,8 +327,16 @@ fn profile_store_upsert_write(path: String, session: AuthenticatedSession, prior } } +// A SKIPPED ENTRY, NAMED: the never-blocks law still holds (one malformed file must not blank the +// picker), but the skip is now carried as data with its reason instead of being erased -- a +// missing-looking avatar must be distinguishable from a profile nobody published (review Cut B). +type ProfileStoreSkipped { + entry: String + reason: String +} + type ProfileStoreReadAll - = ProfileStoreRead { profiles: List } + = ProfileStoreRead { profiles: List, skipped: List } | ProfileStoreUnread { step: String, reason: String } // THE READ-ALL, the picker roster's source: every decodable profile file in the store. A file @@ -340,29 +348,45 @@ fn profile_store_read_all(layout: ProfileStoreLayout) -> ProfileStoreReadAll { if !listing_result.success { ProfileStoreUnread { step: "list", reason: listing_result.error } } else { + let decoded = profile_store_decode_entries(layout: layout, entries: listing_result.entries, acc: [], skipped_acc: []) ProfileStoreRead { - profiles: profile_store_decode_entries(layout: layout, entries: listing_result.entries, acc: []) + profiles: decoded.profiles, + skipped: decoded.skipped, } } } -fn profile_store_decode_entries(layout: ProfileStoreLayout, entries: List, acc: List) -> List { +fn profile_store_decode_entries( + layout: ProfileStoreLayout, + entries: List, + acc: List, + skipped_acc: List, +) -> { profiles: List, skipped: List } { match entries.first() { - Absent => acc + Absent => { + let done = { profiles: acc, skipped: skipped_acc } + done + } Present { value: entry } => - let next = if !ends_with(s: entry, suffix: ".json") { - acc + let stepped = if !ends_with(s: entry, suffix: ".json") { + { profiles: acc, skipped: skipped_acc } } else { let read_result = Filesystem.Read(path: join([layout.root as String, "/", entry], "")) if !read_result.success { - acc + { profiles: acc, skipped: concat(skipped_acc, [ProfileStoreSkipped { entry: entry, reason: join(["read failed: ", read_result.error], "") }]) } } else { match principal_profile_of_json(text: read_result.content) { - ProfileFileMalformed { detail: _ } => acc - ProfileFileDecoded { profile: p } => concat(acc, [p]) + ProfileFileMalformed { detail } => { + let skipped_file = { profiles: acc, skipped: concat(skipped_acc, [ProfileStoreSkipped { entry: entry, reason: join(["malformed: ", detail], "") }]) } + skipped_file + } + ProfileFileDecoded { profile: p } => { + let decoded_file = { profiles: concat(acc, [p]), skipped: skipped_acc } + decoded_file + } } } } - profile_store_decode_entries(layout: layout, entries: entries.skip(n: 1), acc: next) + profile_store_decode_entries(layout: layout, entries: entries.skip(n: 1), acc: stepped.profiles, skipped_acc: stepped.skipped) } } diff --git a/dag/gunbc/harness/harness_cli.dag b/dag/gunbc/harness/harness_cli.dag index d9445787e1f..72405a4d87e 100644 --- a/dag/gunbc/harness/harness_cli.dag +++ b/dag/gunbc/harness/harness_cli.dag @@ -12,7 +12,7 @@ import std.measure { TokenCount, token_count, token_count_value, Second, second, import std.process { ProcessExit, ExitSuccess, ExitFailure } import gunbc.harness.harness_tool { HarnessToolEnvironment } import gunbc.harness.harness_turn { - HarnessTurnConfig, HarnessTurnOutcome, TurnProviderStopped, TurnCompletedByArtifact, harness_tool_contract_wire, + HarnessTurnConfig, HarnessTurnOutcome, TurnProviderStopped, TurnCompletedByArtifact, TurnAlignmentCheckpointDue, harness_tool_contract_wire, CompletesOnProviderStop, CompletesWhenEntryPresent, harness_run_turn, harness_outcome_label, harness_outcome_detail, } @@ -234,6 +234,7 @@ fn harness_macbook_config(backend_url: String, model: NonEmptyStr, unit: Harness reasoning_mode: harness_reasoning_mode, wire_shape: harness_default_wire_shape, system_prompt: harness_default_system_prompt, + alignment_chain_fingerprint: "", request_scratch_path: harness_request_scratch_path, events_path: harness_probe_events_path, context_budget: harness_context_budget_for(unit: unit), @@ -262,11 +263,16 @@ fn harness_probe_outcome(backend_url: String, model: NonEmptyStr, unit: HarnessS // happened rather than whether the task succeeded. Nothing here inspects the assistant's text for // correctness: that is external verification's subject, and a harness that graded its own worker's // answer would be the same conflation DCH-2 forbids between a stop reason and a completed task. +// CHECKPOINT-DUE IS A CONTROL TRANSFER, NOT A FAILURE (review 5358022153): it gets its own admitted +// exit standing (3) rather than the generic failure code, so a consumer matching on 1 never reads a +// planned adjudication handoff as a crash. The belt's authority remains the provider event stream. fn harness_turn_exit(outcome: HarnessTurnOutcome) -> ProcessExit { match outcome { TurnProviderStopped { wire_stop_reason: _, assistant_text: _, steps_taken: _, input_tokens: _, output_tokens: _ } => ExitSuccess TurnCompletedByArtifact { path: _, steps_taken: _ } => ExitSuccess + TurnAlignmentCheckpointDue { steps_taken: _ } => + ExitFailure { code: 3, reason: harness_outcome_label(outcome: outcome) } _ => ExitFailure { code: 1, @@ -508,6 +514,7 @@ fn harness_worker_config( model: NonEmptyStr, unit: HarnessServingUnit, system_prompt: String, + alignment_chain_fingerprint: String, ) -> HarnessTurnConfig { HarnessTurnConfig { backend_url: backend_url, @@ -517,6 +524,7 @@ fn harness_worker_config( reasoning_mode: harness_reasoning_mode, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, + alignment_chain_fingerprint: alignment_chain_fingerprint, request_scratch_path: request_scratch_path, events_path: events_path, context_budget: harness_context_budget_for(unit: unit), @@ -566,6 +574,7 @@ fn harness_worker_cli( repo_root: String, gunbc_program: String, attempt_identity: String, + alignment_chain_fingerprint: String, ) -> ProcessExit { let system_prompt = harness_worker_system_prompt_at(worktree: worktree, repo_root: repo_root, gunbc_program: gunbc_program) let binding = harness_bind_seat(short_hostname: harness_executor_short_hostname(), class: BatchQualityTolerant, requirement: AnyAdmittedModel, shape: harness_default_wire_shape, inputs: harness_model_request_inputs(role: "worker", shape: harness_default_wire_shape, system_prompt: system_prompt, user_input: brief), actor: join(["worker:", worktree], "") as NonEmptyStr, attempt: attempt_identity as NonEmptyStr, policy: harness_seat_policy) @@ -583,6 +592,7 @@ fn harness_worker_cli( model: bound_model, unit: bound_unit, system_prompt: system_prompt, + alignment_chain_fingerprint: alignment_chain_fingerprint, ), prompt: brief, step_budget: harness_worker_step_budget, @@ -631,6 +641,7 @@ fn harness_reviewer_config( reasoning_mode: harness_reasoning_mode, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, + alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, context_budget: harness_context_budget_for(unit: unit), @@ -712,6 +723,7 @@ fn harness_auditor_config( reasoning_mode: harness_reasoning_mode, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, + alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, context_budget: harness_context_budget_for(unit: unit), @@ -802,6 +814,7 @@ fn harness_supervisor_config( reasoning_mode: harness_reasoning_mode, wire_shape: harness_default_wire_shape, system_prompt: system_prompt, + alignment_chain_fingerprint: "", request_scratch_path: request_scratch_path, events_path: events_path, context_budget: harness_context_budget_for(unit: unit), diff --git a/dag/gunbc/harness/harness_conversation_fork.dag b/dag/gunbc/harness/harness_conversation_fork.dag new file mode 100644 index 00000000000..d034b12b08b --- /dev/null +++ b/dag/gunbc/harness/harness_conversation_fork.dag @@ -0,0 +1,118 @@ +module gunbc.harness.harness_conversation_fork + +import std.types { String, Int, Bool } + +// THE FORK POINT: WHERE AN ATTEMPT LINEAGE COULD RESUME. A turn's conversation lives in memory +// while it runs and is gone when the process exits; the fork point is the durable address of a +// moment in that conversation -- which lineage, which epoch and round, and the digests of the +// exact request and response bytes that passed the wire (gunbc.harness.harness_turn's wire log +// persists those bytes under .wire/.{request,response}.json; the digests here +// bind them without carrying the bytes). +type ConversationForkPoint { + attempt_lineage: String + epoch: Int + round: Int + request_digest: String + response_digest: String + model_identity: String + wire_shape: String +} + +// THE FORK RECEIPT'S LAW, stated where admittance is decided. The durable wire log is the +// CORRECTNESS AUTHORITY for what the conversation was: a resume replays context from those bytes +// or it does not happen. An engine's KV prefix residency is a PERFORMANCE DERIVATION from that +// log -- a cache of computed state that may be present, absent, or gone -- and is NEVER a +// correctness dependency: no admittance below consults it, and no resume may require it. A fork is +// admitted only while every identity that produced the context still holds; any change refuses +// with the cause named, because replaying context across a change silently substitutes work the +// engine never did. +type ConversationForkAdmittance + = ForkAdmitted { point: ConversationForkPoint } + | ForkInvalid { cause: String } + +// THE THREE REFUSALS, one per identity that must hold. The causes are the law's sentences: a fork +// across a model change replays context the engine never computed; a fork across a wire-shape +// change replays a conversation the current envelope cannot carry back to the engine; and a moved +// chain fingerprint means the alignment chain the context was built against changed. Each cause +// carries both spellings -- the fork point's and the candidate resume's -- so the refusal names +// what moved instead of merely asserting that something did. +fn conversation_fork_admit( + point: ConversationForkPoint, + model_identity: String, + wire_shape: String, + chain_fingerprint_at_point: String, + chain_fingerprint_now: String, +) -> ConversationForkAdmittance { + if point.model_identity != model_identity { + ForkInvalid { cause: join([ + "a fork across a model change replays context the engine never computed: the fork point was", + " built under model identity ", point.model_identity, + " and the candidate resume offers ", model_identity, + ], "") } + } else if point.wire_shape != wire_shape { + ForkInvalid { cause: join([ + "a fork across a wire-shape change replays a conversation the current envelope cannot carry:", + " the fork point was built under ", point.wire_shape, + " and the candidate resume offers ", wire_shape, + ], "") } + } else if chain_fingerprint_at_point != chain_fingerprint_now { + ForkInvalid { cause: join([ + "the alignment chain the context was built against changed: the fork point binds chain", + " fingerprint ", chain_fingerprint_at_point, + " and the candidate resume offers ", chain_fingerprint_now, + ], "") } + } else { + ForkAdmitted { point: point } + } +} + +// KV PREFIX RESIDENCY IS AN OBSERVATION, NEVER CORRECTNESS STATE. The fabric's cache standing -- +// whether a serving unit still holds the prefix of a conversation in its KV cache -- informs +// PLACEMENT PREFERENCE ONLY: a warm compatible location is PREFERRED (cheaper, faster), and when +// none is available the lineage executes COLD FROM DURABLE STATE, replaying the wire log the slow +// way. Neither direction is a correctness input: a wrong preference costs time, and a wrong +// correctness dependency would cost truth. +// +// THE OBSERVATION'S IDENTITIES ARE THE REASON. A fork receipt binds model, checkpoint, tokenizer, +// chat-template, tool-schema, projection and prefix digests -- the full set a KV prefix silently +// encodes -- because two serving units only share a prefix when ALL of them match. A shared chain +// fingerprint alone NEVER establishes the same KV prefix: the fingerprint names the alignment +// chain the context was built against, not the token stream any particular engine resident holds. +// Standing is therefore carried beside those identities, and a consumer that wants to trust a warm +// location must check every one of them, not the chain name. +type KvPrefixResidency + = Resident + | NotResident + | Unobserved + | Evicted + | Incompatible + +type KvPrefixResidencyObservation { + serving_unit: String + model_identity: String + tokenizer_identity: String + chat_template_identity: String + semantic_prompt_digest: String + wire_digest: String + standing: KvPrefixResidency + observed_at: String +} + +// THE PLACEMENT RULE, pure so the preference is testable without the fabric: only an observed +// Resident prefix earns preference. Unobserved is the honest default -- an unasked question never +// answers warm, so it lands on cold exactly like NotResident, Evicted and Incompatible. Executing +// cold is always available because the durable wire log, not any cache, is the correctness +// authority; this fold decides speed, never possibility. +type KvPlacementPreference + = PreferWarmLocation + | ExecuteColdFromDurableState + +fn kv_prefix_residency_placement(standing: KvPrefixResidency) -> KvPlacementPreference { + match standing { + Resident => PreferWarmLocation + NotResident => ExecuteColdFromDurableState + Unobserved => ExecuteColdFromDurableState + Evicted => ExecuteColdFromDurableState + Incompatible => ExecuteColdFromDurableState + } +} diff --git a/dag/gunbc/harness/harness_guidance.dag b/dag/gunbc/harness/harness_guidance.dag index 63a5140e639..1436bc26eff 100644 --- a/dag/gunbc/harness/harness_guidance.dag +++ b/dag/gunbc/harness/harness_guidance.dag @@ -7,6 +7,9 @@ import gunbc.roadmap.roadmap_submission { submission_artifact_name, submission_schema, submission_text, submission_readiness_key, WorkerSubmission, SubmissionReady, SubmissionBlocked, SubmissionNeedsContext, } +import gunbc.harness.harness_prompt_context { + PromptInput, AuthorityInput, DerivedConclusionInput, PromptSegment, prompt_segment_render, +} // THE PROMPT IS A SYLLOGISM: GROUNDED PREMISES IN, DERIVED CONCLUSIONS OUT. // @@ -67,6 +70,7 @@ type HarnessConclusion | VerifyWithTheTool | ReadingAloneCompletesNothing | ActBeforeTheRoundsRun { rounds: Int } + | RoundsEndInAlignmentCheck | ReRunForDroppedOutput { rounds: Int } | TestWithTheProcess { command: String } @@ -204,6 +208,8 @@ fn harness_conclusion_text(c: HarnessConclusion) -> String { "So if ", to_string(value: n), " rounds will not cover both more investigation and the change, make the change now.", ], "") + RoundsEndInAlignmentCheck => + "When this execution epoch reaches its round boundary, the harness returns control for an independent alignment checkpoint. This is not task completion or task failure. The workflow may resume the same lineage with guidance." ReRunForDroppedOutput { rounds: n } => join([ "So re-run a command if you need output from one of those ", @@ -258,34 +264,84 @@ fn first_rounds(rows: List, f: fn(HarnessPremise) -> Int // THE INFERENCE. Each conclusion names the premises that entail it, so removing a premise removes // the imperative rather than leaving a sentence telling the model to do something unsupported. -fn harness_conclusions(rows: List) -> List { - let writes = any_premise(rows: rows, f: premise_asserts_write_form) - let differs = any_premise(rows: rows, f: premise_requires_difference) - let accepts = any_premise(rows: rows, f: premise_requires_acceptance) +// THE CONCLUSION'S EXACT GROUNDS (review 5358022153): a conclusion derived from two premise kinds +// does not depend on the whole population, and recording every premise as grounds would make a +// moved test-process premise invalidate the write-form conclusion it never fed. The node carries +// the population it was actually derived from; the conclusion's text is a projection of the node, +// never a second spelling. +type HarnessConclusionNode { + conclusion: HarnessConclusion + grounds: List +} + +fn harness_conclusion_key(c: HarnessConclusion) -> String { + match c { + ChangeTheTreeWithTheTool => "change-the-tree-with-the-tool" + VerifyWithTheTool => "verify-with-the-tool" + ReadingAloneCompletesNothing => "reading-alone-completes-nothing" + ActBeforeTheRoundsRun { rounds: _ } => "act-before-the-rounds-run" + RoundsEndInAlignmentCheck => "rounds-end-in-alignment-check" + ReRunForDroppedOutput { rounds: _ } => "rerun-for-dropped-output" + TestWithTheProcess { command: _ } => "test-with-the-process" + } +} + +fn harness_grounds_of(matching: List) -> List { + map(matching, r => r.grounds) +} + +fn harness_conclusion_nodes(rows: List) -> List { + let write_rows = filter(rows, r => premise_asserts_write_form(p: r.premise)) + let differ_rows = filter(rows, r => premise_requires_difference(p: r.premise)) + let accept_rows = filter(rows, r => premise_requires_acceptance(p: r.premise)) + let rounds_rows = filter(rows, r => match premise_rounds_remaining(p: r.premise) { + Present { value: _ } => true + Absent => false + }) + let dropped_rows = filter(rows, r => match premise_rounds_dropped(p: r.premise) { + Present { value: _ } => true + Absent => false + }) + let test_rows = filter(rows, r => match premise_test_process_command(p: r.premise) { + Present { value: _ } => true + Absent => false + }) + let writes = count(write_rows) > 0 + let differs = count(differ_rows) > 0 + let accepts = count(accept_rows) > 0 concat( concat( - if writes && differs { [ChangeTheTreeWithTheTool, ReadingAloneCompletesNothing] } else { [] }, - if accepts { [VerifyWithTheTool] } else { [] }, + if writes && differs { + [ + HarnessConclusionNode { conclusion: ChangeTheTreeWithTheTool, grounds: harness_grounds_of(matching: concat(write_rows, differ_rows)) }, + HarnessConclusionNode { conclusion: ReadingAloneCompletesNothing, grounds: harness_grounds_of(matching: concat(write_rows, differ_rows)) }, + ] + } else { [] }, + if accepts { [HarnessConclusionNode { conclusion: VerifyWithTheTool, grounds: harness_grounds_of(matching: accept_rows) }] } else { [] }, ), concat( concat( match first_rounds(rows: rows, f: premise_rounds_remaining) { - Present { value: n } => [ActBeforeTheRoundsRun { rounds: n }] + Present { value: n } => [HarnessConclusionNode { conclusion: ActBeforeTheRoundsRun { rounds: n }, grounds: harness_grounds_of(matching: rounds_rows) }] Absent => [] }, match first_rounds(rows: rows, f: premise_rounds_dropped) { - Present { value: n } => [ReRunForDroppedOutput { rounds: n }] + Present { value: n } => [HarnessConclusionNode { conclusion: ReRunForDroppedOutput { rounds: n }, grounds: harness_grounds_of(matching: dropped_rows) }] Absent => [] }, ), match first_test_process(rows: rows) { - Present { value: c } => if accepts { [TestWithTheProcess { command: c }] } else { [] } + Present { value: c } => if accepts { [HarnessConclusionNode { conclusion: TestWithTheProcess { command: c }, grounds: harness_grounds_of(matching: concat(test_rows, accept_rows)) }] } else { [] } Absent => [] }, ), ) } +fn harness_conclusions(rows: List) -> List { + map(harness_conclusion_nodes(rows: rows), n => n.conclusion) +} + fn harness_guidance_prose(rows: List) -> String { join( concat( @@ -296,6 +352,36 @@ fn harness_guidance_prose(rows: List) -> String { ) } +// THE MODELED SEGMENTS (harness-prompt-context-model's first consumer). Every grounded premise +// renders as a segment whose source names the declaration it is grounded on at the caller's source +// revision; every conclusion renders as a segment whose source names the conclusion and its +// grounds, so a retracted premise moves the conclusion's input digest with it. The prose above is +// the render projection of exactly this population -- the parity witness +// (test.claim.harness_guidance_test) pins that they cannot fork -- and the digests are what let a +// later PromptPlan say precisely which guidance a worker was told. +fn harness_guidance_segments(rows: List, source_revision: String) -> List { + concat( + map(rows, r => prompt_segment_render( + purpose: "grounded premise", + source: AuthorityInput { declaration: r.grounds, revision: source_revision, required: true }, + content: harness_premise_text(p: r.premise), + )), + map(harness_conclusion_nodes(rows: rows), n => prompt_segment_render( + purpose: "derived conclusion", + source: DerivedConclusionInput { + conclusion_key: harness_conclusion_key(c: n.conclusion), + grounds: n.grounds, + required: true, + }, + content: harness_conclusion_text(c: n.conclusion), + )), + ) +} + +fn harness_guidance_prose_from_segments(segments: List) -> String { + join(map(segments, s => s.content), " ") +} + fn grounded(premise: HarnessPremise, module_path: String, decl_name: String) -> HarnessGroundedPremise { HarnessGroundedPremise { premise: premise, diff --git a/dag/gunbc/harness/harness_prompt_context.dag b/dag/gunbc/harness/harness_prompt_context.dag new file mode 100644 index 00000000000..badfe7a3809 --- /dev/null +++ b/dag/gunbc/harness/harness_prompt_context.dag @@ -0,0 +1,340 @@ +module gunbc.harness.harness_prompt_context + +import std.types { String, Bool, Int, List, NonEmptyStr } +import std.decl_ref { DeclarationRef } +import std.content_hash { content_hash_atom } +import gunbc.roadmap_model { RoadmapNodeId } + +// A PROMPT IS A PROJECTION OF AN ADMITTED CONTEXT SNAPSHOT, not an authority and never an authored +// string handed across a boundary (scoped review against 81e7c779, 2026-09-29; invariant repairs +// from review 5358022153). This module models the snapshot and its inputs; provider envelopes +// project from the plan LATE, and the digest receipts make "what the model was told" +// reconstructable from durable inputs after any restart. WHAT THE TYPES GUARD AND WHAT THEY DO +// NOT: input provenance, standing, and revision are typed; segment CONTENT is rendered prose, +// digested but not structured. The value is that inputs are typed and revisioned -- a stale +// authority revision invalidates the render, a withdrawn item cannot render -- not that the prose +// is machine-checked. Semantic contradiction between two free-text inputs is not inferred here; a +// conflict must be typed to refuse. +// +// THE STANDING OF A CONTEXT ITEM. Withdrawn exists so a retracted assumption cannot linger in a +// conversation cache and resurface as a fact: it is a first-class state, not a deletion. +type ContextStanding + = EstablishedObservation + | DerivedConclusion + | OwnerRuling + | InstructionInput + | Hypothesis + | OpenQuestion + | Withdrawn + +fn context_standing_key(standing: ContextStanding) -> String { + match standing { + EstablishedObservation => "established-observation" + DerivedConclusion => "derived-conclusion" + OwnerRuling => "owner-ruling" + InstructionInput => "instruction" + Hypothesis => "hypothesis" + OpenQuestion => "open-question" + Withdrawn => "withdrawn" + } +} + +// THE TYPED PROMPT INPUT. Every segment of a rendered prompt traces to one of these; an input +// carries the REVISION of its authority so a changed authority invalidates any snapshot that +// consumed it, rather than the old render silently persisting. required marks the undroppable +// population (active work contract, latest instruction, unresolved guidance, owner rulings, the +// current revision); compaction and assembly both refuse rather than lose one. +type PromptInput + = AuthorityInput { declaration: DeclarationRef, revision: String, required: Bool } + | WorkContractInput { node: RoadmapNodeId, revision: String, required: Bool } + | EvidenceInput { artifact: String, subject: String, standing: ContextStanding, required: Bool } + | ConversationTurnInput { turn_identity: String, parent_identity: String, required: Bool } + | GuidanceInput { receipt_identity: String, resolved: Bool, required: Bool } + | DerivedConclusionInput { conclusion_key: String, grounds: List, required: Bool } + +// THE CANONICAL PREIMAGE IS LENGTH-PREFIXED, never separator-joined (review 5358022153): a field +// containing a separator cannot manufacture the same preimage as another field sequence, and every +// semantic field -- standing, resolution, requiredness -- participates, so a Hypothesis that +// becomes an EstablishedObservation is a DIFFERENT input, not the same one restated. +fn prompt_field_enc(s: String) -> String { + join([to_string(value: string_length(s: s)), ":", s], "") +} + +fn prompt_bool_enc(b: Bool) -> String { + if b { "1:t" } else { "1:f" } +} + +fn prompt_decl_ref_enc(d: DeclarationRef) -> String { + join([ + prompt_field_enc(s: d.module_path as String), + prompt_field_enc(s: d.decl_name as String), + ], "") +} + +fn prompt_input_canonical_text(input: PromptInput) -> String { + match input { + AuthorityInput { declaration, revision, required } => + join(["18:authority-input/v2", prompt_decl_ref_enc(d: declaration), prompt_field_enc(s: revision), prompt_bool_enc(b: required)], "") + WorkContractInput { node, revision, required } => + join(["20:work-contract-input/v2", prompt_field_enc(s: node as String), prompt_field_enc(s: revision), prompt_bool_enc(b: required)], "") + EvidenceInput { artifact, subject, standing, required } => + join(["16:evidence-input/v2", prompt_field_enc(s: artifact), prompt_field_enc(s: subject), prompt_field_enc(s: context_standing_key(standing: standing)), prompt_bool_enc(b: required)], "") + ConversationTurnInput { turn_identity, parent_identity, required } => + join(["24:conversation-turn-input/v2", prompt_field_enc(s: turn_identity), prompt_field_enc(s: parent_identity), prompt_bool_enc(b: required)], "") + GuidanceInput { receipt_identity, resolved, required } => + join(["15:guidance-input/v2", prompt_field_enc(s: receipt_identity), prompt_bool_enc(b: resolved), prompt_bool_enc(b: required)], "") + DerivedConclusionInput { conclusion_key, grounds, required } => + join(concat( + ["25:derived-conclusion-input/v2", prompt_field_enc(s: conclusion_key)], + map(grounds, g => prompt_decl_ref_enc(d: g)), + [prompt_bool_enc(b: required)], + ), "") + } +} + +fn prompt_input_required(input: PromptInput) -> Bool { + match input { + AuthorityInput { declaration: _, revision: _, required } => required + WorkContractInput { node: _, revision: _, required } => required + EvidenceInput { artifact: _, subject: _, standing: _, required } => required + ConversationTurnInput { turn_identity: _, parent_identity: _, required } => required + GuidanceInput { receipt_identity: _, resolved: _, required } => required + DerivedConclusionInput { conclusion_key: _, grounds: _, required } => required + } +} + +fn prompt_input_is_withdrawn(input: PromptInput) -> Bool { + match input { + EvidenceInput { artifact: _, subject: _, standing, required: _ } => + match standing { + Withdrawn => true + _ => false + } + _ => false + } +} + +// THE SNAPSHOT: one immutable binding of inputs to an attempt lineage at an epoch and a source +// revision. The alignment probes of one checkpoint share a snapshot identity and receive different +// PROJECTIONS of it; they never share an undifferentiated conversation. +type PromptContextSnapshot { + attempt_lineage: String + epoch: Int + source_revision: String + work_contract_revision: String + inputs: List +} + +fn prompt_snapshot_digest(snapshot: PromptContextSnapshot) -> String { + content_hash_atom(value: join([ + "25:prompt-context-snapshot/v2", + prompt_field_enc(s: snapshot.attempt_lineage), + to_string(value: snapshot.epoch), + ":", + prompt_field_enc(s: snapshot.source_revision), + prompt_field_enc(s: snapshot.work_contract_revision), + join(map(snapshot.inputs, i => prompt_input_canonical_text(input: i)), ""), + ], "") as NonEmptyStr).digest as String +} + +// A RENDERED SEGMENT: typed source, declared purpose, and the content digested at render time. +// required is derived from the source input, never asserted beside it, so a segment cannot claim +// undroppability its source does not have. +type PromptSegment { + purpose: String + source: PromptInput + content: String + content_digest: String +} + +fn prompt_segment_render(purpose: String, source: PromptInput, content: String) -> PromptSegment { + PromptSegment { + purpose: purpose, + source: source, + content: content, + content_digest: content_hash_atom(value: join(["18:prompt-segment/v2", prompt_field_enc(s: purpose), prompt_field_enc(s: content)], "") as NonEmptyStr).digest as String, + } +} + +fn prompt_segment_required(segment: PromptSegment) -> Bool { + prompt_input_required(input: segment.source) +} + +type PromptPlan { + snapshot: PromptContextSnapshot + snapshot_digest: String + ordered_segments: List +} + +// THE ASSEMBLY REFUSALS, one per way the join can lie (review 5358022153). Assembly is an exact +// join between the snapshot's eligible inputs and their renderings: a plan exists only when every +// non-withdrawn input rendered exactly once, no withdrawn input rendered at all, and no segment's +// source is foreign to the snapshot. +type PromptAssemblyRefusal + = RequiredSourceUnrendered { input_key: String } + | MissingRendering { input_key: String } + | DuplicateRendering { input_key: String } + | UnexpectedRendering { source_key: String } + | WithdrawnSourceRendered { input_key: String } + +fn prompt_assembly_refusal_text(r: PromptAssemblyRefusal) -> String { + match r { + RequiredSourceUnrendered { input_key } => join(["a required input never rendered: ", input_key], "") + MissingRendering { input_key } => join(["an eligible input never rendered: ", input_key], "") + DuplicateRendering { input_key } => join(["an input rendered more than once: ", input_key], "") + UnexpectedRendering { source_key } => join(["a rendered segment's source is not an input of this snapshot: ", source_key], "") + WithdrawnSourceRendered { input_key } => join(["a withdrawn input reached the rendered plan: ", input_key], "") + } +} + +type PromptPlanAssembly + = PromptPlanReady { plan: PromptPlan } + | PromptPlanRefused { refusals: List } + +fn prompt_assembly_input_fold( + rendered: List, + acc_in: { refusals: List, segments: List }, + input: PromptInput, +) -> { refusals: List, segments: List } { + let key = prompt_input_canonical_text(input: input) + let matching = filter(rendered, seg => prompt_input_canonical_text(input: seg.source) == key) + if prompt_input_is_withdrawn(input: input) { + if count(matching) > 0 { + { refusals: concat(acc_in.refusals, [WithdrawnSourceRendered { input_key: key }]), segments: acc_in.segments } + } else { + acc_in + } + } else if count(matching) == 0 { + if prompt_input_required(input: input) { + { refusals: concat(acc_in.refusals, [RequiredSourceUnrendered { input_key: key }]), segments: acc_in.segments } + } else { + { refusals: concat(acc_in.refusals, [MissingRendering { input_key: key }]), segments: acc_in.segments } + } + } else if count(matching) > 1 { + { refusals: concat(acc_in.refusals, [DuplicateRendering { input_key: key }]), segments: acc_in.segments } + } else { + match matching.first() { + Absent => acc_in + Present { value: seg } => { + let kept = { refusals: acc_in.refusals, segments: concat(acc_in.segments, [seg]) } + kept + } + } + } +} + +// THE ASSEMBLY LAW: the plan's ordered segments are derived from the snapshot's input order -- the +// caller's render order cannot reorder the prompt. Withdrawn inputs produce ZERO segments. Any +// refusal at all refuses the plan; there is no partial prompt. +fn prompt_plan_assemble( + snapshot: PromptContextSnapshot, + rendered: List, +) -> PromptPlanAssembly { + let joined = fold( + snapshot.inputs, + init: { refusals: [] as List, segments: [] as List }, + f: fn(acc, inp) { prompt_assembly_input_fold(rendered: rendered, acc_in: acc, input: inp) }, + ) + let foreign = filter(rendered, seg => + !any(snapshot.inputs, i => prompt_input_canonical_text(input: i) == prompt_input_canonical_text(input: seg.source))) + let foreign_refusals = map(foreign, seg => UnexpectedRendering { source_key: prompt_input_canonical_text(input: seg.source) }) + let all_refusals = concat(joined.refusals, foreign_refusals) + if count(all_refusals) > 0 { + PromptPlanRefused { refusals: all_refusals } + } else { + PromptPlanReady { + plan: PromptPlan { + snapshot: snapshot, + snapshot_digest: prompt_snapshot_digest(snapshot: snapshot), + ordered_segments: joined.segments, + }, + } + } +} + +// EXACT PROMPT BOUNDARIES (review 5358022153). Each extent names its source input and the rendered +// span. The basis is declared honestly: the substrate's string_length counts characters for +// non-ASCII text, so until a UTF-8 byte primitive exists the durable basis is char-count -- the +// field is what keeps a future byte-exact realization distinguishable from this fallback rather +// than silently reinterpreting old extents. +type SemanticPromptExtent { + source_input_key: String + start: Int + end: Int + content_digest: String +} + +type SemanticPromptArtifact { + text: String + semantic_digest: String + extent_basis: String + extents: List +} + +fn semantic_prompt_render_fold(acc_in: { text: String, extents: List }, segment: PromptSegment) -> { text: String, extents: List } { + let start = if acc_in.text == "" { 0 } else { string_length(s: acc_in.text) + 1 } + let text = if acc_in.text == "" { segment.content } else { join([acc_in.text, "\n", segment.content], "") } + { + text: text, + extents: concat(acc_in.extents, [SemanticPromptExtent { + source_input_key: prompt_input_canonical_text(input: segment.source), + start: start, + end: string_length(s: text), + content_digest: segment.content_digest, + }]), + } +} + +fn semantic_prompt_render(plan: PromptPlan) -> SemanticPromptArtifact { + let rendered = fold( + plan.ordered_segments, + init: { text: "", extents: [] as List }, + f: fn(acc, segment) { semantic_prompt_render_fold(acc_in: acc, segment: segment) }, + ) + SemanticPromptArtifact { + text: rendered.text, + semantic_digest: content_hash_atom(value: join(["24:semantic-prompt-artifact/v1", rendered.text], "") as NonEmptyStr).digest as String, + extent_basis: "char-count-fallback", + extents: rendered.extents, + } +} + +// THE PROVIDER PROJECTION'S OWN ARTIFACT: the semantic digest is shared across providers; the wire +// digest is over the exact request bytes of THIS envelope, so an Anthropic and an OpenAI rendering +// of one plan share a semantic identity and never share a wire identity. +type ProviderPromptArtifact { + semantic_digest: String + provider_projection: String + wire_text: String + wire_digest: String +} + +fn provider_prompt_artifact(semantic: SemanticPromptArtifact, provider_projection: String, wire_text: String) -> ProviderPromptArtifact { + ProviderPromptArtifact { + semantic_digest: semantic.semantic_digest, + provider_projection: provider_projection, + wire_text: wire_text, + wire_digest: content_hash_atom(value: join(["24:provider-prompt-artifact/v1", prompt_field_enc(s: provider_projection), wire_text], "") as NonEmptyStr).digest as String, + } +} + +// THE RENDER RECEIPT, one per provider projection: which snapshot, which inputs, and the digests +// of exactly what rendered. Cache-hit and cache-miss renders of the same plan must produce the +// same semantic digest -- residency is a performance fact, never part of correctness. +type PromptRenderReceipt { + plan_snapshot_digest: String + input_digests: List + semantic_digest: String + provider_projection: String + wire_digest: String +} + +fn prompt_render_receipt(plan: PromptPlan, wire: ProviderPromptArtifact) -> PromptRenderReceipt { + PromptRenderReceipt { + plan_snapshot_digest: plan.snapshot_digest, + input_digests: map(plan.snapshot.inputs, i => content_hash_atom(value: prompt_input_canonical_text(input: i) as NonEmptyStr).digest as String), + semantic_digest: wire.semantic_digest, + provider_projection: wire.provider_projection, + wire_digest: wire.wire_digest, + } +} diff --git a/dag/gunbc/harness/harness_turn.dag b/dag/gunbc/harness/harness_turn.dag index aeb8276bc11..a2e26cd9a5e 100644 --- a/dag/gunbc/harness/harness_turn.dag +++ b/dag/gunbc/harness/harness_turn.dag @@ -29,7 +29,7 @@ import gunbc.roadmap.roadmap_submission { } import gunbc.harness.harness_guidance { HarnessPremise, RoundsRemaining, EarlierRoundsDropped, harness_premise_text, - HarnessConclusion, ActBeforeTheRoundsRun, ReRunForDroppedOutput, harness_conclusion_text, + HarnessConclusion, ActBeforeTheRoundsRun, RoundsEndInAlignmentCheck, ReRunForDroppedOutput, harness_conclusion_text, } import gunbc.harness.harness_wire { HarnessWireShape, AnthropicMessagesShape, OpenAiChatCompletionsShape, harness_decode_for_shape, @@ -228,6 +228,14 @@ type HarnessTurnConfig { reasoning_mode: HarnessReasoningLevel wire_shape: HarnessWireShape system_prompt: String + // THE CHAIN FINGERPRINT THE ATTEMPT SPAWNED WITH, composed at dispatch from the node's resolved + // alignment chain (gunbc.roadmap.roadmap_alignment alignment_resolve). Empty means this turn has + // no chain -- reviewers, probes and ad-hoc runs -- and the step budget stays a hard terminal, the + // pre-chain behavior. Non-empty turns the budget boundary into a durable checkpoint-due + // announcement: the harness does NOT adjudicate (no provider call, no worker self-report); it + // records the due event bound to this fingerprint and returns control to the workflow, whose + // adjudication lane decides resume, steer, re-plan, route or stop (review 5354823380). + alignment_chain_fingerprint: String request_scratch_path: FilePath events_path: FilePath context_budget: TokenCount @@ -261,8 +269,10 @@ fn harness_context_exhausted(input_tokens: TokenCount, budget: TokenCount) -> Bo // THE HARNESS EMITS THE PROVIDER EVENT SCHEMA ITSELF, rather than having its liveness reconstructed // from outside. gunbc.roadmap_provider_events already declares what an attempt's event stream is -- -// turn.started, item.started, item.completed, turn.completed, turn.budget_exhausted, turn.failed, error, read line by line -// through jq -- and every consumer of agent evidence is written against it. A second stream shape +// turn.started, item.started, item.completed, turn.completed, turn.budget_exhausted, turn.failed, +// error, round.usage, the checkpoint announcement turn.alignment_checkpoint_due and the wire-log +// refusal turn.wire_log_unwritable, read line by line through jq -- and every consumer of agent +// evidence is written against it. A second stream shape // for this one provider would be the section 3 fork; emitting the existing one makes the harness a // first-class provider on a contract that already has readers. // @@ -401,6 +411,14 @@ fn harness_event_turn_budget_exhausted(steps_taken: Int) -> String { ])) } +fn harness_event_alignment_checkpoint_due(steps_taken: Int, chain_fingerprint: String) -> String { + serialize_json(v: json_object([ + json_kv(key: "type", value: json_string(s: "turn.alignment_checkpoint_due")), + json_kv(key: "steps_taken", value: json_int(n: steps_taken)), + json_kv(key: "chain_fingerprint", value: json_string(s: chain_fingerprint)), + ])) +} + fn harness_event_turn_failed(message: String) -> String { serialize_json(v: json_object([ json_kv(key: "type", value: json_string(s: "turn.failed")), @@ -410,6 +428,19 @@ fn harness_event_turn_failed(message: String) -> String { ])) } +// THE WIRE LOG'S REFUSAL IS AN EVENT, NOT A TERMINAL. The event stream is the terminal record of a +// turn; the wire log is recoverable evidence -- its bytes can be reconstructed from durable state, +// and its absence degrades diagnosis, never the work. Killing the turn on a logging write would +// trade the work for the receipt, so a refused wire-log write is recorded here with the host's +// detail and the round proceeds exactly as it would have. The reader (gunbc.roadmap_provider_events) +// admits it as non-terminal with provider state unchanged, following the round.usage arm. +fn harness_event_wire_log_unwritable(detail: String) -> String { + serialize_json(v: json_object([ + json_kv(key: "type", value: json_string(s: "turn.wire_log_unwritable")), + json_kv(key: "detail", value: json_string(s: detail)), + ])) +} + fn harness_tool_round_events(uses: List) -> List { fold( uses, @@ -473,6 +504,7 @@ type HarnessTurnOutcome | TurnThinkingFormatInvalid { cause: String, steps_taken: Int } | TurnUsageUnavailable { cause: String, steps_taken: Int } | TurnSubmissionUndecodable { reason: String, steps_taken: Int } + | TurnAlignmentCheckpointDue { steps_taken: Int } fn harness_outcome_label(outcome: HarnessTurnOutcome) -> String { match outcome { @@ -490,6 +522,7 @@ fn harness_outcome_label(outcome: HarnessTurnOutcome) -> String { TurnThinkingFormatInvalid { cause: _, steps_taken: _ } => "thinking-format-invalid" TurnUsageUnavailable { cause: _, steps_taken: _ } => "usage-unavailable" TurnSubmissionUndecodable { reason: _, steps_taken: _ } => "submission-undecodable" + TurnAlignmentCheckpointDue { steps_taken: _ } => "alignment-checkpoint-due" } } @@ -539,6 +572,13 @@ fn harness_outcome_detail(outcome: HarnessTurnOutcome) -> String { join([ "after ", to_string(value: n), " tool rounds: ", harness_submission_terminal_cause(reason: reason), ], "") + TurnAlignmentCheckpointDue { steps_taken: n } => + join([ + "after ", to_string(value: n), + " tool rounds the epoch boundary announced an alignment checkpoint; the turn returned", + " control to the workflow, whose adjudication lane decides whether the same lineage", + " resumes, steers, re-plans, routes or stops. This is not a failure and not a completion.", + ], "") TurnTransportRefused { detail: d } => d TurnResponseUnreadable { cause: c } => c TurnRequestUnwritable { path: p, detail: d } => @@ -761,17 +801,27 @@ fn harness_wire_block_json(block: HarnessWireBlock) -> JsonValue { // It rides on the tool-result message rather than a new message because the pairing after the // opening brief must stay assistant/user: an extra turn between them would break the alternation // the compaction logic depends on. -fn harness_budget_notice_text(steps_remaining: Int) -> String { +// +// THE CONCLUSION DEPENDS ON WHAT ZERO MEANS. A chainless turn ends at zero, so the notice presses +// to act before the rounds run; a chain-carrying turn meets an alignment checkpoint at zero that +// refreshes the count on a pass, so the notice says THAT -- telling such a worker the turn is +// ending would be a fabricated deadline, and the pressure it bought would be pressure toward a +// finish line that does not exist. +fn harness_budget_notice_text(steps_remaining: Int, checkpointed: Bool) -> String { join([ harness_premise_text(p: RoundsRemaining { rounds: steps_remaining }), " ", - harness_conclusion_text(c: ActBeforeTheRoundsRun { rounds: steps_remaining }), + if checkpointed { + harness_conclusion_text(c: RoundsEndInAlignmentCheck) + } else { + harness_conclusion_text(c: ActBeforeTheRoundsRun { rounds: steps_remaining }) + }, ], "") } -fn harness_budget_notice_block(steps_remaining: Int) -> JsonValue { +fn harness_budget_notice_block(steps_remaining: Int, checkpointed: Bool) -> JsonValue { json_object([ json_kv(key: "type", value: json_string(s: "text")), - json_kv(key: "text", value: json_string(s: harness_budget_notice_text(steps_remaining: steps_remaining))), + json_kv(key: "text", value: json_string(s: harness_budget_notice_text(steps_remaining: steps_remaining, checkpointed: checkpointed))), ]) } @@ -779,6 +829,7 @@ fn harness_tool_result_messages_json( shape: HarnessWireShape, results: List, steps_remaining: Int, + checkpointed: Bool, ) -> List { match shape { AnthropicMessagesShape => @@ -787,7 +838,7 @@ fn harness_tool_result_messages_json( json_kv(key: "content", value: json_array( elements: concat( map(results, r => harness_tool_result_block(tool_use_id: r.tool_use_id, text: r.text, is_error: r.is_error)), - [harness_budget_notice_block(steps_remaining: steps_remaining)], + [harness_budget_notice_block(steps_remaining: steps_remaining, checkpointed: checkpointed)], ), )), ])] @@ -800,7 +851,7 @@ fn harness_tool_result_messages_json( ])), [json_object([ json_kv(key: "role", value: json_string(s: "user")), - json_kv(key: "content", value: json_string(s: harness_budget_notice_text(steps_remaining: steps_remaining))), + json_kv(key: "content", value: json_string(s: harness_budget_notice_text(steps_remaining: steps_remaining, checkpointed: checkpointed))), ])], ) } @@ -933,6 +984,33 @@ fn harness_thinking_format_cause(unit: HarnessServingUnit, blocks: List String { + join([config.events_path as String, ".wire/", to_string(value: round), kind], "") +} + // THE LEVEL IS RESOLVED AGAINST THE UNIT BEFORE A BYTE IS WRITTEN, and a unit that cannot express // the requested level stops the line here rather than sending its nearest neighbour. The refusal is // typed, located and carries both names, so the run reports which unit refused which level instead @@ -940,16 +1018,20 @@ fn harness_thinking_format_cause(unit: HarnessServingUnit, blocks: List, -) -> HarnessResponseRead { + round: Int, +) -> HarnessPostedRound { match harness_reasoning_kwargs_for(unit: config.serving_unit, level: config.reasoning_mode) { ReasoningLevelUnsupported { unit, level, cause } => - HarnessResponseUnreadable { - cause: join([ - "the serving unit ", unit as String, " cannot express reasoning level ", level as String, - ": ", cause as String, - ], ""), + HarnessPostedRound { + read: HarnessResponseUnreadable { + cause: join([ + "the serving unit ", unit as String, " cannot express reasoning level ", level as String, + ": ", cause as String, + ], ""), + }, + wire_log_detail: none, } - ReasoningKwargsAdmitted { kwargs } => harness_post_round_admitted(config: config, messages: messages, reasoning_kwargs: kwargs) + ReasoningKwargsAdmitted { kwargs } => harness_post_round_admitted(config: config, messages: messages, reasoning_kwargs: kwargs, round: round) } } @@ -957,38 +1039,64 @@ fn harness_post_round_admitted( config: HarnessTurnConfig, messages: List, reasoning_kwargs: List, -) -> HarnessResponseRead { + round: Int, +) -> HarnessPostedRound { let body = serialize_json(v: harness_request_json(config: config, messages: messages, reasoning_kwargs: reasoning_kwargs)) let written = Filesystem.Write(path: config.request_scratch_path as String, content: body) if !written.success { - HarnessResponseUnreadable { - cause: join([ - "request body could not be written to ", config.request_scratch_path as String, - ": ", written.error, - ], ""), + HarnessPostedRound { + read: HarnessResponseUnreadable { + cause: join([ + "request body could not be written to ", config.request_scratch_path as String, + ": ", written.error, + ], ""), + }, + wire_log_detail: none, } } else { + let wire_request_path = harness_wire_log_path(config: config, round: round, kind: ".request.json") + let wire_request = Filesystem.Write(path: wire_request_path, content: body) let posted = http.Client.PostJsonFromFile( url: config.backend_url as NonEmptyStr, request_body_file: config.request_scratch_path as String as NonEmptyStr, max_seconds: to_string(value: second_count(s: config.request_deadline)) as NonEmptyStr, ) + let wire_response_path = harness_wire_log_path(config: config, round: round, kind: ".response.json") + let wire_response = Filesystem.Write(path: wire_response_path, content: posted.body) let kept = Filesystem.Write( path: join([config.request_scratch_path as String, ".response.json"], ""), content: posted.body, ) + let wire_details = concat( + if wire_request.success { [] } else { [join(["request wire log ", wire_request_path, " could not be written: ", wire_request.error], "")] }, + if wire_response.success { [] } else { [join(["response wire log ", wire_response_path, " could not be written: ", wire_response.error], "")] }, + ) + let wire_log_detail = if count(wire_details) == 0 { + none + } else { + Present { value: join(wire_details, "; ") } + } if !posted.success { - HarnessResponseUnreadable { - cause: join([ - "the backend refused the request or the transport failed; body was: ", posted.body, - ], ""), + HarnessPostedRound { + read: HarnessResponseUnreadable { + cause: join([ + "the backend refused the request or the transport failed; body was: ", posted.body, + ], ""), + }, + wire_log_detail: wire_log_detail, } } else if !kept.success { - HarnessResponseUnreadable { - cause: join(["the response could not be kept beside the request: ", kept.error, "; refusing to decode a body no one can re-read"], ""), + HarnessPostedRound { + read: HarnessResponseUnreadable { + cause: join(["the response could not be kept beside the request: ", kept.error, "; refusing to decode a body no one can re-read"], ""), + }, + wire_log_detail: wire_log_detail, } } else { - harness_decode_for_shape(unit: config.serving_unit, shape: config.wire_shape, body: posted.body) + HarnessPostedRound { + read: harness_decode_for_shape(unit: config.serving_unit, shape: config.wire_shape, body: posted.body), + wire_log_detail: wire_log_detail, + } } } } @@ -1087,6 +1195,31 @@ fn harness_event_write_refusal(config: HarnessTurnConfig, detail: String) -> Har } } +// THE ALIGNMENT CHECKPOINT, ANNOUNCED, NOT ADJUDICATED. At the epoch boundary a chain-carrying +// turn does not call the provider again and does not ask the worker to grade itself: it records +// that adjudication is due, bound to the chain fingerprint the attempt spawned with, and returns +// control to the workflow durably (review 5354823380). The belt's adjudication lane -- independent +// of this process -- reads the due event, examines the evidence, publishes the verdict and the next +// cadence state, and only then resumes the same lineage. The harness's whole obligation here is +// that the announcement is durable and the outcome is typed: a checkpoint is never a failure, a +// completion, or a submission. +fn harness_turn_checkpoint_due( + config: HarnessTurnConfig, + steps_taken: Int, + events: List, +) -> HarnessTurnOutcome { + let announced = concat(events, [ + harness_event_alignment_checkpoint_due( + steps_taken: steps_taken, + chain_fingerprint: config.alignment_chain_fingerprint, + ), + ]) + match harness_write_events(config: config, events: announced) { + HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) + HarnessEventsWritten => TurnAlignmentCheckpointDue { steps_taken: steps_taken } + } +} + fn harness_turn_loop( config: HarnessTurnConfig, messages: List, @@ -1096,15 +1229,24 @@ fn harness_turn_loop( repairs_used: Int, ) -> HarnessTurnOutcome { if steps_remaining <= 0 { - let exhausted = concat(events, [harness_event_turn_budget_exhausted(steps_taken: steps_taken)]) - match harness_write_events(config: config, events: exhausted) { - HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) - HarnessEventsWritten => TurnStepBudgetExhausted { steps_taken: steps_taken } + if config.alignment_chain_fingerprint == "" { + let exhausted = concat(events, [harness_event_turn_budget_exhausted(steps_taken: steps_taken)]) + match harness_write_events(config: config, events: exhausted) { + HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) + HarnessEventsWritten => TurnStepBudgetExhausted { steps_taken: steps_taken } + } + } else { + harness_turn_checkpoint_due(config: config, steps_taken: steps_taken, events: events) } } else { - match harness_post_round(config: config, messages: messages) { + let posted_round = harness_post_round(config: config, messages: messages, round: steps_taken + 1) + let events_after_wire = match posted_round.wire_log_detail { + Present { value: detail } => concat(events, [harness_event_wire_log_unwritable(detail: detail)]) + Absent => events + } + match posted_round.read { HarnessResponseUnreadable { cause: c } => { - let failed = concat(events, [harness_event_turn_failed(message: c)]) + let failed = concat(events_after_wire, [harness_event_turn_failed(message: c)]) match harness_write_events(config: config, events: failed) { HarnessEventsUnwritable { detail: d } => harness_event_write_refusal(config: config, detail: d) HarnessEventsWritten => TurnResponseUnreadable { cause: c } @@ -1113,7 +1255,7 @@ fn harness_turn_loop( HarnessResponseParsed { blocks: raw_blocks, stop_reason, usage: usage_read } => match usage_read { UsageUnreadable { cause: uc } => { - let unavailable = concat(events, [ + let unavailable = concat(events_after_wire, [ harness_event_round_usage_unavailable(step: steps_taken + 1, cause: uc, clock: clock_unix_millis_read()), harness_event_turn_failed( message: join([ @@ -1131,7 +1273,7 @@ fn harness_turn_loop( harness_turn_round( config: config, messages: messages, raw_blocks: raw_blocks, stop_reason: stop_reason, input_tokens: input_tokens, output_tokens: output_tokens, - steps_remaining: steps_remaining, steps_taken: steps_taken, events: events, + steps_remaining: steps_remaining, steps_taken: steps_taken, events: events_after_wire, repairs_used: repairs_used, ) } @@ -1387,6 +1529,7 @@ fn harness_turn_continue( shape: config.wire_shape, results: results, steps_remaining: steps_remaining - 1, + checkpointed: config.alignment_chain_fingerprint != "", ), ), ), diff --git a/dag/gunbc/roadmap/roadmap_alignment.dag b/dag/gunbc/roadmap/roadmap_alignment.dag index 337af735d90..1477d4867ae 100644 --- a/dag/gunbc/roadmap/roadmap_alignment.dag +++ b/dag/gunbc/roadmap/roadmap_alignment.dag @@ -22,7 +22,7 @@ import extdeps.languages.json.parse { json_object_unique_member, JsonMemberFound, } -import gunbc.roadmap_nesting { RoadmapMembership, declared_roadmap_memberships } +import gunbc.roadmap_nesting { RoadmapMembership, declared_roadmap_memberships, declared_roadmap_roots } import gunbc.roadmap_model { RoadmapNode, RoadmapNodeId } import gunbc.roadmap_status { TicketLine } import gunbc.roadmap_authority { declared_roadmap_nodes } @@ -40,11 +40,23 @@ import gunbc.roadmap.roadmap_attempt_continuation { import std.content_hash { content_hash_atom } // The recursive alignment ladder follows authored nesting, never reverse blocking. -// Each level carries its declared purpose and boundary. Ambiguous ownership refuses. +// Each level carries the node's declared ticket structure and the edge's contribution argument. +// Ambiguous ownership refuses. +// +// THE CARRIER PRESERVES THE TICKET, it does not reduce it (review 5354823380): purpose and +// boundary alone forced every level to answer from two fields, while TicketFields already +// declares the current approach, the red control, the handback and the displaced cost a +// checkpoint must be read against. contribution_to_parent is the edge's argument -- why THIS +// child's work advances THIS parent -- carried on the level it explains. type AlignmentChainLink { node_id: String purpose: String boundary: String + current_approach: String + red_control: String + handback: String + displaced_cost: String + contribution_to_parent: String } // THE TERMINAL LEVEL IS A POINTER, not text: the root principles are gunbc.design_document, and @@ -61,6 +73,11 @@ type AlignmentChain at: String, dependents: List, } + | AlignmentChainUnrooted { + walked: List, + at: String, + } + | AlignmentChainContributionRefused { member: String, project: String, detail: String } | AlignmentChainRowUndeclared { node_id: String } | AlignmentChainCyclic { node_id: String } @@ -77,7 +94,7 @@ type AlignmentLinkProjection = LinkProjected { link: AlignmentChainLink } | LinkRowUndeclared { node_id: String } -fn alignment_link_project(node_id: String, nodes: List) -> AlignmentLinkProjection { +fn alignment_link_project(node_id: String, nodes: List, contribution_to_parent: String) -> AlignmentLinkProjection { let found = nodes |> filter(n => (n.node as String) == node_id) if count(found) != 1 { LinkRowUndeclared { node_id: node_id } @@ -92,6 +109,11 @@ fn alignment_link_project(node_id: String, nodes: List) -> Alignmen node_id: node_id, purpose: ticket.headline, boundary: ticket.brief, + current_approach: ticket.current_state, + red_control: ticket.red_control, + handback: ticket.handback, + displaced_cost: ticket.displaced_cost, + contribution_to_parent: contribution_to_parent, }, } _ => LinkRowUndeclared { node_id: node_id } @@ -100,13 +122,59 @@ fn alignment_link_project(node_id: String, nodes: List) -> Alignmen } } +// THE EDGE'S ARGUMENT for one member under one project, read fail-closed (review 5358022153): +// the empty string used to mean both "the admitted root has no parent" and "no contribution was +// authored", so a missing edge could read as root membership. Rootness now comes only from +// declared_roadmap_roots(); a non-root link requires exactly one edge with a non-empty +// contribution, and a duplicated edge with disagreeing contributions refuses rather than picking +// a first. +type AlignmentContributionRead + = AlignmentContributionResolved { contribution: String } + | AlignmentContributionMissing { member: String, project: String } + | AlignmentContributionVacuous { member: String, project: String } + | AlignmentContributionConflicting { member: String, project: String } + +fn alignment_membership_contribution_read(memberships: List, member: String, project: String) -> AlignmentContributionRead { + let rows = memberships |> filter(m => m.member == member && m.project == project) + if count(rows) == 0 { + AlignmentContributionMissing { member: member, project: project } + } else if count(rows) > 1 { + match rows.first() { + Absent => AlignmentContributionMissing { member: member, project: project } + Present { value: first_row } => + if all(rows, r => r.contribution == first_row.contribution) { + if first_row.contribution == "" { + AlignmentContributionVacuous { member: member, project: project } + } else { + AlignmentContributionResolved { contribution: first_row.contribution } + } + } else { + AlignmentContributionConflicting { member: member, project: project } + } + } + } else { + match rows.first() { + Absent => AlignmentContributionMissing { member: member, project: project } + Present { value: row } => + if row.contribution == "" { + AlignmentContributionVacuous { member: member, project: project } + } else { + AlignmentContributionResolved { contribution: row.contribution } + } + } + } +} + // THE WALK STATE. halted freezes the accumulator for the remaining fuel; ambiguous_dependents is // non-empty exactly when MembershipAmbiguous stopped the climb (the typed refusal carries WHICH // arcs forked); undeclared_halt names the node whose row could not be projected ("" = none). The -// two never co-occur, and a clean standalone halt leaves both empty. +// two never co-occur, and a clean halt leaves both empty. projects carries the walked ancestor IDS +// nearest-first; links with their edge contributions are built once the whole id chain is known, +// because an edge's contribution argument belongs to the level BELOW it and is only knowable when +// the next step exists. type AlignmentWalk { current: String - projects: List + projects: List halted: Bool ambiguous_dependents: List undeclared_halt: String @@ -135,13 +203,13 @@ fn alignment_walk_step( MembershipStandalone => alignment_walk_halt(acc: acc, ambiguous_dependents: [], undeclared_halt: "") MembershipAmbiguous { arcs } => alignment_walk_halt(acc: acc, ambiguous_dependents: arcs, undeclared_halt: "") MembershipMember { arc } => - match alignment_link_project(node_id: arc, nodes: nodes) { + match alignment_link_project(node_id: arc, nodes: nodes, contribution_to_parent: "") { LinkRowUndeclared { node_id: missing } => alignment_walk_halt(acc: acc, ambiguous_dependents: [], undeclared_halt: missing) - LinkProjected { link } => + LinkProjected { link: _ } => AlignmentWalk { current: arc, - projects: concat(acc.projects, [link]), + projects: concat(acc.projects, [arc]), halted: false, ambiguous_dependents: [], undeclared_halt: "", @@ -151,15 +219,126 @@ fn alignment_walk_step( } } +// LINK CONSTRUCTION over one walked id chain: ids[0] is the task, ids[i+1] is ids[i]'s project, +// and each link's contribution_to_parent is the argument of the edge that leaves it. The chain's +// top -- the admitted root project -- has no parent edge and carries the empty contribution. +// Row projection is fail-closed here exactly as in the walk; the rows were already validated +// during the walk, so a refusal here means the authority moved mid-derivation. +// LINK CONSTRUCTION'S ANSWER, typed: a refused edge contribution (missing, vacuous, or +// conflicting on a non-root link) cannot become the empty string that used to read as rootness. +type AlignmentLinksBuild + = AlignmentLinksBuilt { links: List } + | AlignmentLinksRowUndeclared + | AlignmentLinksContributionRefused { member: String, project: String, detail: String } + +fn alignment_links_for_ids( + ids: List, + memberships: List, + nodes: List, +) -> AlignmentLinksBuild { + let built = fold( + ids, + init: { links: [] as List, index: 0, undeclared: false, refused_member: "", refused_project: "", refused_detail: "" }, + f: fn(acc, id) { + if acc.undeclared || acc.refused_member != "" { + acc + } else { + let contribution_read = if acc.index + 1 < count(ids) { + match alignment_list_string_at(items: ids, index: acc.index + 1) { + Present { value: parent } => + alignment_membership_contribution_read(memberships: memberships, member: id, project: parent) + Absent => AlignmentContributionMissing { member: id, project: "" } + } + } else { + AlignmentContributionResolved { contribution: "" } + } + match contribution_read { + AlignmentContributionResolved { contribution } => + match alignment_link_project(node_id: id, nodes: nodes, contribution_to_parent: contribution) { + LinkRowUndeclared { node_id: _ } => { + let refused_state = { links: acc.links, index: acc.index + 1, undeclared: true, refused_member: acc.refused_member, refused_project: acc.refused_project, refused_detail: acc.refused_detail } + refused_state + } + LinkProjected { link } => { + let kept_state = { links: concat(acc.links, [link]), index: acc.index + 1, undeclared: false, refused_member: "", refused_project: "", refused_detail: "" } + kept_state + } + } + AlignmentContributionMissing { member, project } => { + let s1 = { links: acc.links, index: acc.index + 1, undeclared: acc.undeclared, refused_member: member, refused_project: project, refused_detail: "no membership edge carries this pair" } + s1 + } + AlignmentContributionVacuous { member, project } => { + let s2 = { links: acc.links, index: acc.index + 1, undeclared: acc.undeclared, refused_member: member, refused_project: project, refused_detail: "the edge's contribution is empty; a non-root level must argue why its work advances the parent" } + s2 + } + AlignmentContributionConflicting { member, project } => { + let s3 = { links: acc.links, index: acc.index + 1, undeclared: acc.undeclared, refused_member: member, refused_project: project, refused_detail: "duplicated membership rows carry disagreeing contributions for one edge" } + s3 + } + } + } + }, + ) + if built.undeclared { + AlignmentLinksRowUndeclared + } else if built.refused_member != "" { + AlignmentLinksContributionRefused { member: built.refused_member, project: built.refused_project, detail: built.refused_detail } + } else { + AlignmentLinksBuilt { links: built.links } + } +} + +fn alignment_link_list_tail(items: List) -> List { + let folded = fold(items, init: { seen: 0, kept: [] as List }, f: fn(acc, item) { + if acc.seen == 0 { + { seen: 1, kept: acc.kept } + } else { + { seen: acc.seen + 1, kept: concat(acc.kept, [item]) } + } + }) + folded.kept +} + +fn alignment_list_string_at(items: List, index: Int) -> String? { + if index < 0 { + none + } else { + let found = fold( + items, + init: { seen: 0, hit: false, value: "" }, + f: fn(acc, item) { + if acc.hit { + acc + } else if acc.seen == index { + { seen: acc.seen + 1, hit: true, value: item } + } else { + { seen: acc.seen + 1, hit: false, value: "" } + } + }, + ) + if found.hit { + Present { value: found.value } + } else { + none + } + } +} + // THE CHAIN. Task level first, then the motivating project chain nearest-first, then the root // principles pointer. The walk's fuel is the declared edges themselves: each fold step consumes one // 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. +// +// THE WALK MUST END AT AN ADMITTED ROOT (review 5354823380). A standalone halt anywhere else means +// the decomposition never reached the top of its own project tree, and the chain refuses as +// Unrooted rather than crowning an arbitrary node. DESIGN.md is not that root: the principles are +// the terminal POINTER every chain carries, orthogonal to the project hierarchy. fn alignment_chain(node_id: String, memberships: List, nodes: List) -> AlignmentChain { - match alignment_link_project(node_id: node_id, nodes: nodes) { + match alignment_link_project(node_id: node_id, nodes: nodes, contribution_to_parent: "") { LinkRowUndeclared { node_id: missing } => AlignmentChainRowUndeclared { node_id: missing } - LinkProjected { link: task } => { + LinkProjected { link: task_without_contribution } => { let walked = fold( memberships, init: AlignmentWalk { @@ -172,20 +351,47 @@ fn alignment_chain(node_id: String, memberships: List, nodes: f: (acc, fuel_edge) => alignment_walk_step(nodes: nodes, memberships: memberships, acc: acc, fuel_edge: fuel_edge), ) if count(walked.ambiguous_dependents) > 0 { - AlignmentChainAmbiguous { - walked: walked.projects, - at: walked.current, - dependents: walked.ambiguous_dependents, + match alignment_links_for_ids(ids: concat([node_id], walked.projects), memberships: memberships, nodes: nodes) { + AlignmentLinksRowUndeclared => AlignmentChainRowUndeclared { node_id: node_id } + AlignmentLinksContributionRefused { member, project, detail } => + AlignmentChainContributionRefused { member: member, project: project, detail: detail } + AlignmentLinksBuilt { links } => + AlignmentChainAmbiguous { + walked: alignment_link_list_tail(items: links), + at: walked.current, + dependents: walked.ambiguous_dependents, + } } - } else if any(walked.projects, p => p.node_id == node_id || count(filter(walked.projects, q => q.node_id == p.node_id)) > 1) { + } else if any(walked.projects, p => p == node_id || count(filter(walked.projects, q => q == p)) > 1) { AlignmentChainCyclic { node_id: node_id } } else if walked.undeclared_halt != "" { AlignmentChainRowUndeclared { node_id: walked.undeclared_halt } + } else if !any(declared_roadmap_roots(), r => r == walked.current) { + match alignment_links_for_ids(ids: concat([node_id], walked.projects), memberships: memberships, nodes: nodes) { + AlignmentLinksRowUndeclared => AlignmentChainRowUndeclared { node_id: node_id } + AlignmentLinksContributionRefused { member, project, detail } => + AlignmentChainContributionRefused { member: member, project: project, detail: detail } + AlignmentLinksBuilt { links } => + AlignmentChainUnrooted { + walked: alignment_link_list_tail(items: links), + at: walked.current, + } + } } else { - AlignmentChained { - task: task, - projects: walked.projects, - root: alignment_root_pointer(), + match alignment_links_for_ids(ids: concat([node_id], walked.projects), memberships: memberships, nodes: nodes) { + AlignmentLinksRowUndeclared => AlignmentChainRowUndeclared { node_id: node_id } + AlignmentLinksContributionRefused { member, project, detail } => + AlignmentChainContributionRefused { member: member, project: project, detail: detail } + AlignmentLinksBuilt { links } => + match links.first() { + Absent => AlignmentChainRowUndeclared { node_id: node_id } + Present { value: task } => + AlignmentChained { + task: task, + projects: alignment_link_list_tail(items: links), + root: alignment_root_pointer(), + } + } } } } @@ -398,13 +604,21 @@ type AlignmentResolvedChain { root: DeclarationRef } +fn alignment_link_list_reverse(items: List) -> List { + fold(items, init: [] as List, f: fn(acc, item) { concat([item], acc) }) +} + +// THE LEVELS OF A CHAIN, ROOT TO LEAF (review 5354823380): the principles pointer first, then the +// root project, each intermediate project in turn, and the task contract last. The cadence still +// counts FROM THE LEAF -- the task aligns most often, each level up an order of magnitude less -- +// so a level's cadence index is its distance from the end of this list, never its position in it. fn alignment_levels(chain: AlignmentResolvedChain) -> List { concat( concat( - [TaskContractLevel { link: chain.task }], - map(chain.projects, p => ProjectIntentLevel { link: p }), + [RootPrinciplesLevel], + map(alignment_link_list_reverse(items: chain.projects), p => ProjectIntentLevel { link: p }), ), - [RootPrinciplesLevel], + [TaskContractLevel { link: chain.task }], ) } @@ -420,12 +634,14 @@ fn alignment_levels_due(chain: AlignmentResolvedChain, turns_elapsed: Int, state if turns_elapsed <= 0 { [] } else { - let due_fold = alignment_levels(chain: chain) + let levels = alignment_levels(chain: chain) + let total = count(levels) + let due_fold = levels |> fold( - init: { due: [], index: 0 }, + init: { due: [] as List, index: 0 }, f: fn(acc, level) { { - due: if turns_elapsed % alignment_state_interval(state: state, level_index: acc.index) == 0 { + due: if turns_elapsed % alignment_state_interval(state: state, level_index: total - 1 - acc.index) == 0 { concat(acc.due, [level]) } else { acc.due @@ -455,6 +671,8 @@ fn alignment_levels_due(chain: AlignmentResolvedChain, turns_elapsed: Int, state // prose. type AlignmentRefusal = AlignmentChainForked { at: String, dependents: List, walked: List } + | AlignmentWalkUnrooted { at: String, walked: List } + | AlignmentEdgeContributionRefused { member: String, project: String, detail: String } | AlignmentRowUndeclared { node_id: String } | AlignmentNestingCyclic { node_id: String } @@ -466,6 +684,18 @@ 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", ], "") + AlignmentWalkUnrooted { at, walked: _ } => + join([ + "the membership walk from the task ended at ", at, + ", which is not an admitted root project (gunbc.roadmap_nesting declared_roadmap_roots);", + " a chain that never reaches the top of its functional decomposition has no alignment answer,", + " so the node must be attached to the hierarchy before it can dispatch", + ], "") + AlignmentEdgeContributionRefused { member, project, detail } => + join([ + "the membership edge ", member, " -> ", project, + " cannot supply the link's contribution argument: ", detail, + ], "") AlignmentNestingCyclic { node_id } => concat("Cyclic project nesting at ", node_id) AlignmentRowUndeclared { node_id } => join([ @@ -496,21 +726,38 @@ type AlignmentResolution // interpretation and never only the immediate L1: the whole derived chain is the brief's alignment // answer. Project levels render nearest-first (the walk's order); the task contract level closes // the ladder; a projectless node names its own row as the intent, the honest standalone shape. +// THE RENDER, ROOT TO LEAF, one per level (review 5354823380). L0 stays a pointer (the load phase +// carries the principles' text; the prompt names their home and no more) and the ask stays bounded +// -- both are the continuation lane's authored cadence rows, imported, so this module owns the +// DERIVATION's rendering and the continuation lane owns its standing text. Every project level +// renders with its carried ticket contribution (approach, boundary, red control) and the edge's +// contribution argument, never a fresh interpretation; the task contract closes the ladder. The +// root project's line names it as the admitted end of the walk. fn alignment_link_line(prefix: String, link: AlignmentChainLink) -> String { - join([prefix, link.node_id, ": ", link.purpose, " -- boundary: ", link.boundary], "") + join([ + prefix, link.node_id, ": ", link.purpose, + "\n approach: ", link.current_approach, + "\n boundary: ", link.boundary, + "\n contribution to parent: ", + if any(declared_roadmap_roots(), r => r == link.node_id) { + "none -- this level is the admitted root project" + } else { + link.contribution_to_parent + }, + "\n red control: ", link.red_control, + "\n handback: ", link.handback, + "\n displaced cost: ", link.displaced_cost, + ], "") } fn alignment_project_lines(projects: List) -> String { - let numbered = fold(projects, init: { out: "", index: 1 }, f: fn(acc, link) { + let ordered = alignment_link_list_reverse(items: projects) + let numbered = fold(ordered, init: { out: "", index: 1 }, f: fn(acc, link) { { out: join([ acc.out, alignment_link_line( - prefix: if acc.index == 1 { - "L1 project intent: this task serves project " - } else { - join(["L1 project intent (level ", to_string(value: acc.index), " above the task): this level serves project "], "") - }, + prefix: join(["L", to_string(value: acc.index), " project intent: "], ""), link: link, ), "\n", @@ -523,34 +770,35 @@ fn alignment_project_lines(projects: List) -> String { fn alignment_chain_render(chain: AlignmentResolvedChain) -> String { join([ - "\nAlignment (re-checked at every continuation turn):\n", + "\nAlignment (re-checked at every checkpoint adjudication):\n", continuation_alignment_l0_pointer, "\n", - if count(chain.projects) == 0 { - join([ - "L1 project intent: this node is a project of one, so its own declared row is the intent: ", - chain.task.purpose, " -- boundary: ", chain.task.boundary, "\n", - ], "") - } else { - alignment_project_lines(projects: chain.projects) - }, - alignment_link_line(prefix: "L2 task contract: ", link: chain.task), "\n", - continuation_alignment_ask, "\n\n", + alignment_project_lines(projects: chain.projects), + alignment_link_line( + prefix: join(["L", to_string(value: count(chain.projects) + 1), " task contract: "], ""), + link: chain.task, + ), + "\n", continuation_alignment_ask, "\n\n", ], "") } // THE CHAIN FINGERPRINT binds the chain to whatever identity carries it: a structural digest over -// the canonical preimage of EVERY link (node id, carried purpose, carried boundary) and the root -// pointer, so any authority edit that moves the chain — a rewired edge, a retitled row, a reworded -// boundary — moves the fingerprint with it. Drift detection, not a security boundary: the +// the canonical preimage of EVERY link (node id, carried purpose, boundary, approach, red control, +// and the edge's contribution argument) and the root pointer, so any authority edit that moves the +// chain — a rewired edge, a retitled row, a reworded boundary, a changed contribution — moves the +// fingerprint with it. Drift detection, not a security boundary: the // Fnv1a64Structural family is the corpus's structural-fingerprint family (std.content_hash), the // preimage is label-prefixed by construction (it begins with a constant), so the atom's NonEmptyStr // input is grounded, never asserted, and the unit separator cannot collide with a field's content. fn alignment_chain_canonical_text(chain: AlignmentResolvedChain) -> String { join([ - "alignment-chain/v1", + "alignment-chain/v2", "task", chain.task.node_id, chain.task.purpose, chain.task.boundary, + chain.task.current_approach, chain.task.red_control, chain.task.handback, + chain.task.displaced_cost, chain.task.contribution_to_parent, fold(chain.projects, init: "", f: fn(acc, link) { - join([acc, "project", link.node_id, link.purpose, link.boundary], "\u{1f}") + join([acc, "project", link.node_id, link.purpose, link.boundary, + link.current_approach, link.red_control, link.handback, link.displaced_cost, + link.contribution_to_parent], "\u{1f}") }), "root", chain.root.module_path as String, chain.root.decl_name as String, ], "\u{1f}") @@ -560,6 +808,35 @@ fn alignment_chain_fingerprint(chain: AlignmentResolvedChain) -> String { content_hash_atom(value: alignment_chain_canonical_text(chain: chain) as NonEmptyStr).digest as String } +// ------------------------------------------------------------------------------------------------ +// The checkpoint identity (review 5358022153). A chain fingerprint alone is not a checkpoint: one +// lineage crosses many checkpoints under the same chain. This record is the idempotency key the +// belt's adjudication lane consumes -- probe obligation creation, verdict publication, roundup +// publication and worker resumption all key on it, so a crash after the roundup publishes but +// before the next epoch starts recovers exactly one resume. +// ------------------------------------------------------------------------------------------------ + +type AlignmentCheckpointIdentity { + attempt_lineage: String + epoch: Int + checkpoint_ordinal: Int + chain_fingerprint: String + conversation_tip: String + candidate_subject: String +} + +fn alignment_checkpoint_identity_digest(id: AlignmentCheckpointIdentity) -> String { + content_hash_atom(value: join([ + "alignment-checkpoint/v1", + id.attempt_lineage, + to_string(value: id.epoch), + to_string(value: id.checkpoint_ordinal), + id.chain_fingerprint, + id.conversation_tip, + id.candidate_subject, + ], "") as NonEmptyStr).digest as String +} + // THE RESOLUTION FOLD, over one derived chain. This is the only crossing from AlignmentChain to a // consumer answer: the Chained arm builds Ready (levels and render computed from the resolved // record), the refusal arms freeze into Unresolved with their typed causes. Fuel, membership and @@ -576,6 +853,10 @@ fn alignment_resolve_chain(chain: AlignmentChain, turns_elapsed: Int, state: Ali } AlignmentChainAmbiguous { walked, at, dependents } => AlignmentUnresolved { cause: AlignmentChainForked { at: at, dependents: dependents, walked: walked } } + AlignmentChainUnrooted { walked, at } => + AlignmentUnresolved { cause: AlignmentWalkUnrooted { at: at, walked: walked } } + AlignmentChainContributionRefused { member, project, detail } => + AlignmentUnresolved { cause: AlignmentEdgeContributionRefused { member: member, project: project, detail: detail } } 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_authority.dag b/dag/gunbc/roadmap/roadmap_authority.dag index 9e6f8ec6f66..d5690d666ed 100644 --- a/dag/gunbc/roadmap/roadmap_authority.dag +++ b/dag/gunbc/roadmap/roadmap_authority.dag @@ -3084,6 +3084,262 @@ fn declared_roadmap_nodes() -> List { handback: "Return the census, the wall or the frozen roster with its dissolution trigger, and both controls." ) ), + active_small( + identity: "rn_GUNBCROOT0PROJECT0FACTORY0A", + id: "gunbc-project-root", + owner: "program", + path: "dag/gunbc/design_document.dag", + t: fields( + headline: "Build the self-hosting, proof-carrying LLM software factory", + boundary: "The admitted root of the functional decomposition: every dispatchable task reaches this node through authored membership, each edge carrying why the child's work advances its parent. DESIGN.md is the principles authority orthogonal to this hierarchy, not a level of it.", + displaced_cost: "Without a declared root, a projectless task reads as its own justification and drift has no chain to be measured against.", + first_slice: "Admit this root, attach the active harness lineage and the shell batch to it, and refuse alignment chains that end anywhere else.", + red_control: "A task whose membership walk ends at a node not declared a root refuses its alignment resolution; this node is the only parentless one the walk may end at.", + out_of_scope: "This node is never dispatched and never carried by an execution contract; it exists so the walk has an admitted end.", + handback: "The rooted hierarchy, the walk's refusal of unrooted chains, and the receipts binding both." + ) + ), + active_small( + identity: "rn_HARNESS0WORK0LIFECYCLE00A", + id: "harness-work-lifecycle", + owner: "harness", + path: "docs/plans/harness-work-lifecycle.md", + t: fields( + headline: "Make the mandatory journey from intake through delivery workflow-owned, durable, recoverable, and autonomous", + boundary: "The steps every code change must traverse -- orientation, implementation, test, commit, publication, review -- are owned by the workflow as durable state, not by an agent's memory of its instructions.", + displaced_cost: "Every obligation left in prose is re-learned per session and silently dropped under pressure; the failures look like agent error but are missing structure.", + first_slice: "Name the mandatory steps of one real worker journey and drag the first of them into a durable, workflow-owned mechanism.", + red_control: "A step the workflow claims to own survives process death and controller restart; one that lives only in a prompt does not count.", + out_of_scope: "No change to what verification or review require; the lane is who owns the journey, not whether it happens.", + handback: "The lifecycle model, the durable state, and the receipt of one journey completed by workflow ownership." + ) + ), + active_small( + identity: "rn_HARNESS0RECURSIVE0ALIGN0A", + id: "harness-recursive-alignment", + owner: "harness", + path: "docs/plans/harness-work-lifecycle.md", + t: fields( + headline: "Allow work to continue across durable epochs while preserving alignment to every project level and the task contract", + boundary: "Continuation is unbounded in time but bounded in grounding: at each epoch boundary an independent adjudication reads the attempt's evidence against its rooted chain and resumes, steers, re-plans, routes, or stops the lineage.", + displaced_cost: "A fixed round budget ends correct work mid-task; uncapped continuation without adjudication trades longer execution for quiet drift. Both failure shapes were measured on live workers.", + first_slice: "Turn the harness's round-count terminal into a checkpoint-due announcement the workflow adjudicates outside the worker loop.", + red_control: "A worker crosses two checkpoints on one lineage and submits one candidate; no checkpoint is reported as task failure or completion; a planted steer is accepted and visible in the next epoch.", + out_of_scope: "The checkpoint never establishes submission, verification, publication, or readiness; those gates are untouched.", + handback: "The checkpoint mechanism, the durable cadence state, the adjudication boundary, and the two-checkpoint acceptance receipt." + ) + ), + active_small( + identity: "rn_HPROMPT0CONTEXT0MODEL00A", + id: "harness-prompt-context-model", + owner: "harness", + path: "dag/gunbc/harness/harness_prompt_context.dag", + t: fields( + headline: "A prompt is a projection of an admitted context snapshot, never an authored string", + boundary: "Prompt inputs are typed (authority declaration + revision, work contract + revision, evidence with standing, prior conversation turns, guidance receipts); a context snapshot binds them to an attempt lineage, epoch, and source revision; provider envelopes are late projections of a PromptPlan; a render receipt digests exactly what was rendered. A withdrawn item cannot render as established; a conversation turn cannot fork from bytes the lineage does not own.", + displaced_cost: "Today the modeled guidance premises collapse into system_prompt: String at the config boundary, so a contradiction between two inputs is concatenated and shipped to the model, and a stale conclusion can resurface from a provider session as if it were a fact.", + first_slice: "Stand up the PromptInput/PromptContextSnapshot/PromptSegment/PromptPlan types with the render receipt, and make harness_guidance emit modeled segments rather than the final system-prompt string.", + red_control: "Identical snapshots render identical digests; changing an authority revision invalidates the old render; a Withdrawn input cannot reach a rendered segment; compaction cannot drop unresolved guidance or a tool result's matching call; restart reconstructs the same prompt from durable inputs.", + out_of_scope: "Segment content is rendered prose, digested -- the type system guards input provenance and revision, not the prose itself. No semantic contradiction inference between free-text inputs; conflicts must be typed to refuse.", + handback: "The snapshot/segment/plan model, the render receipt, harness_guidance emitting segments, and the controls." + ) + ), + active_small( + identity: "rn_HCHECKPOINT0PLANNER000A", + id: "harness-checkpoint-subject-planner", + owner: "harness", + path: "dag/gunbc/roadmap/roadmap_alignment.dag", + t: fields( + headline: "Freeze the checkpoint subject and derive the per-level probe obligations, purely", + boundary: "Given an AlignmentCheckpointIdentity and the resolved chain, derive the homogeneous probe population (principles, root project, each intermediate project, task), each obligation carrying its level contract, the edge contribution it judges against, the prior guidance outstanding at that level, and the PromptContextSnapshot identity it projects from. No model call, no workspace effect, no verdict: this is the pure plan the durable scheduler executes.", + displaced_cost: "If probe obligations are derived inside the execution loop, a crash mid-fan-out cannot say which probes existed, and reruns risk minting duplicate verdicts for one checkpoint.", + first_slice: "The obligation-derivation function and its witness: one checkpoint over the harness lineage derives exactly four obligations in root-to-leaf order, each keyed by the checkpoint identity.", + red_control: "The same checkpoint identity derives the identical obligation set (idempotent re-derivation after a crash); an obligation's level contract digest and contribution digest come from the chain, not re-authored; a checkpoint whose chain fingerprint moved refuses re-derivation.", + out_of_scope: "No probe execution, no roundup, no resume; those are the scheduler and adjudication folds. No PromptPlan rendering here either -- the probes' plans are assembled where they execute.", + handback: "The derivation, the obligation carrier, the idempotency witness, and the join key the scheduler consumes." + ) + ), + active_small( + identity: "rn_HCONVO0LOG0FORK0POINTS0A", + id: "harness-conversation-log-forking", + owner: "harness", + path: "dag/gunbc/harness/harness_turn.dag", + t: fields( + headline: "The attempt lineage owns its conversation log; epochs resume and probes fork from typed fork points", + boundary: "Every round's exact wire (messages sent, blocks received) persists under the attempt's state directory keyed by round, so any (attempt, epoch, round) names an exact conversation state. Epoch resume replays the log to its tip and appends the guidance bundle; an alignment probe forks at the checkpoint's log tip: shared chain prefix plus one level-specific suffix branch. The engine's prefix cache only accelerates what the log already makes correct; nothing may depend on the cache being warm.", + displaced_cost: "Without a durable log, continuation re-briefs blind, a worker's hard-won context dies with its process, and 'fork from a known point' is impossible because no known point exists.", + first_slice: "Persist per-round request/response bodies keyed by round in the attempt state directory; wire continuation resume to replay from the log instead of a fresh brief.", + red_control: "A fork is typed: ForkAdmitted { tip } / ForkInvalid { cause }, where a model, weights, chat-template, or chain-fingerprint change invalidates the fork point; a crashed worker's next epoch resumes from the durable log, not from a re-authored brief; a probe can name the exact log tip it forked from.", + out_of_scope: "No engine-side KV pinning or replica affinity; the cache stays an opportunistic derivation. No change to what the alignment probes judge.", + handback: "The durable log, the fork-point typing, resume-from-log, and receipts for crash recovery and invalidation." + ) + ), + active_small( + identity: "rn_HALIGN0CHECKPOINT0RESUME0A", + id: "harness-alignment-checkpoint-resume", + owner: "harness", + path: "dag/gunbc/harness/harness_turn.dag", + t: fields( + headline: "Replace fixed round exhaustion with an evidence-bound checkpoint whose verdict resumes, steers, re-plans, routes, or stops the same attempt lineage", + boundary: "At the epoch boundary the harness emits turn.alignment_checkpoint_due bound to the chain fingerprint and returns control; adjudication runs outside the worker loop against durable evidence; the verdict and next cadence state publish atomically; only adjudication resumes the lineage.", + displaced_cost: "An in-loop checkpoint that asks the worker to grade itself mints continuation on the worker's own word -- the drift the checkpoint exists to catch is exactly the drift such a worker would not report.", + first_slice: "Typed TurnAlignmentCheckpointDue outcome plus the durable event, the admitted-root chain projection, and the belt's adjudication-and-resume wiring.", + red_control: "An unrooted task refuses its chain; a worker's own aligned assertion cannot mint a continuation receipt; a changed node contract or edge contribution invalidates earlier receipts; a crash after verdict publication produces exactly one next epoch.", + out_of_scope: "No change to submission, verification, or publication gates; reviewers, auditors and probes keep hard step budgets.", + handback: "The outcome, the rooted projection, the adjudication boundary, durable cadence state, and witnesses for the red controls." + ) + ), + + // THE NATIVE-SCM AUTHORITY PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The + // dag-scm-design rows are one program (operator ruling 2026-09-13): native SCM becomes the + // authority for authoring, proposals, integration and history, with Git and GitHub demoted to + // compatibility projections. The three upstream models pin each system's own vocabulary, the + // compatibility shape extracts their shared roles, the P0 landing spine and P1 proof kernel + // make submit-to-landed real, and the native-authority rows cut the bridge over. This node is + // that shared alignment level; the bound row attaches to it, and the remaining rows attach as + // their execution contracts land. + active_small( + identity: "rn_1317A022JY9TLMSN5C9F142XA", + id: "dag-scm-program", + owner: "source-intent", + path: "docs/plans/dag-scm-design.md", + t: fields( + headline: "Make native SCM the authority, Git a projection", + boundary: "The native-SCM program: describe Git, Mercurial, and Pijul on their own terms and extract the compatibility realization shape from those receipts; carry one submitted change from result-finding through permission checks, committing, independent read-back, and compatibility projection; prove one bounded reconciliation rule for directly authored changes with data-driven invariants and certificates; and land the native authority -- CAS slot consumers, Git import wall, bridge cutover -- so authoring, proposals, integration and history no longer route through Git. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "While Git stays the hidden authority, every native refusal has a fallback that makes the PR the source of truth exactly when the native path is under stress, and the SCM domain model stays without a production authoring, submission, or landing route.", + first_slice: "Read the rows in the program's authored order: the three upstream models, the claim-indexed evidence carrier, the compatibility shape, the P0 landing spine, the P1 proof kernel, then the native write-spine acceptance and native-authority rows.", + red_control: "A dag-scm row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted dag-scm chain, every attached child's edge contribution readable against this intent, and no dag-scm row whose walk ends anywhere else." + ) + ), + + // THE NAMESPACE PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The namespace lane's rows + // -- occurrence transport, binding kernel, structural observations, reference-derived closure, + // ambiguity discharge, the canonical changeover, and the consumer/import/retirement rows -- are + // one functional decomposition of a single claim: what a name means is worked out from where it + // is written and decided one canonical way. This node is that shared alignment level; the bound + // rows attach to it, and the remaining rows attach as their execution contracts land. + active_small( + identity: "rn_4K7LFFQ3V1B0NIZA5HUGNWWUD", + id: "namespace-unique-on-chain", + owner: "namespace", + path: "docs/plans/namespace-unique-on-chain-operational-plan.md", + t: fields( + headline: "Decide every name one canonical way", + boundary: "The namespace program: carry occurrences exactly; decide none/one/many with one kernel handed the candidates; observe what a name can see on ordinary compiles; derive what each file depends on from those answers; discharge the genuinely-many cases from real compilation; land the canonical changeover and retire the older deciding mechanisms. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "Several mechanisms deciding the same name differently is how a program silently comes to mean something other than what was written; one reviewable decision is the correctness surface every later guarantee reads through.", + first_slice: "Read the rows in the lane's authored order: namespace-occurrence-transport, namespace-binding-kernel, namespace-structural-observations, namespace-reference-derived-closure, namespace-ambiguity-discharge, namespace-canonical-binding, then the consumer, import, and retirement rows.", + red_control: "A namespace row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted namespace chain, every attached child's edge contribution readable against this intent, and no namespace row whose walk ends anywhere else." + ) + ), + + // THE V1 DELETION PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The v1_deletion_plan + // rows are one program: enumerate what still depends on the old interpreter and old emitter, + // observe the hand-written population from the host instead of transcribing it, carry test + // coverage across migration, and record the trust ruling that gates removal -- so deletion is a + // derived population and a recorded decision, never a judgment call. This node is that shared + // alignment level; the bound rows attach to it, and the remaining rows attach as their + // execution contracts land. + active_small( + identity: "rn_WNFHYN3C1DA917SQJMLCTUFX3", + id: "v1-deletion-program", + owner: "self-host", + path: "dag/gunbc/v1/v1_deletion_plan.dag", + t: fields( + headline: "Delete the old compiler honestly", + boundary: "The v1 deletion program: the interpreter's primitive surface becomes enumerable and then model-authoritative; the hand-written Rust population is read from the repository at a known revision rather than transcribed; deleting old tests loses no coverage; the products build and run with the old directory absent; and the seed-honesty ruling records whether independent rebuild must precede removal. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "'Mostly migrated' is not a state anyone can verify; without a derived frontier and a recorded trust ruling, deletion either waits forever or lands silently incomplete.", + first_slice: "Read the rows in dependency order: the primitive-surface roster and dispatch authority, the host source observation, the honest-frontier census, test migration, the seed-honesty decision, then the queue drain, emitter deletion, and the v1-free product builds.", + red_control: "A deletion-program row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted deletion chain, every attached child's edge contribution readable against this intent, and no v1-deletion row whose walk ends anywhere else." + ) + ), + + // THE V2 EMITTER PRODUCTION LANE (review 5358022153 lane B, 2026-09-30). The self-host claim has + // a measurable half: the v2 compiler writes the Rust the factory is built from, and every + // produced result records which generator made it. The two bound rows of that lane attach + // here; the remaining self-host rows attach as their execution contracts land. + active_small( + identity: "rn_YYXCOYJGPXICGC4YYL8TPFQH9", + id: "v2-emitter-production", + owner: "self-host", + path: "src/v2/compiler/self_host/candidate_generation.dag", + t: fields( + headline: "The new compiler writes its own Rust, with provenance", + boundary: "The emitter production lane: the v2 compiler reads source, works out the types, translates, and writes real Rust without the old generator deciding any of it; and every produced result says which generator made it and how far it got, as two separate facts that cannot be claimed ahead of their receipts. This node is the lane's decomposition level; the executable rows are its children.", + displaced_cost: "Until the new compiler can write Rust on its own, every improvement routes through the generator this lane exists to delete; and without per-generator provenance the transition cannot be measured, only asserted.", + first_slice: "Read the rows in dependency order: the producer-provenance roster, the direct-rust-door production walk, then the behavioral-module, compatible-corpus, native-crate-partition, and native-bootstrap rows.", + red_control: "An emitter row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this lane.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted emitter chain, every attached child's edge contribution readable against this intent, and no emitter row whose walk ends anywhere else." + ) + ), + + // THE CI END-TO-END EXECUTION PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The + // ci-process-end-to-end rows are one program: CI owns its execution, and a verdict names the + // work that produced it. The bound row attaches here; the remaining rows attach as their + // execution contracts land. + active_small( + identity: "rn_GG8OZ558D0UB3R98N5MQCDPKB", + id: "ci-end-to-end-execution", + owner: "ci-control", + path: "docs/plans/ci-process-end-to-end.md", + t: fields( + headline: "CI owns its execution end to end", + boundary: "The CI process program: every required check carries the population it actually executed -- which witnesses, which corpus, how many -- a run that executed nothing refuses to report any verdict, and a failure ahead of the corpus is its own outcome, distinct from the corpus running and disagreeing. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "A check that spent days reporting failures while executing no corpus at all looked exactly like a real disagreement from outside; without the executed population on the verdict, CI output is evidence about pipeline health rather than about the corpus, and a genuine signal is invisible.", + first_slice: "Read the rows in authored order: the executed-coverage receipt, the check projection, actions-runner retirement, and remote-build containment.", + red_control: "A CI-program row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted CI chain, every attached child's edge contribution readable against this intent, and no CI-program row whose walk ends anywhere else." + ) + ), + + // THE PROGRESS-OBSERVATION PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The + // progress-observation-design rows are one program: progress reaches operators through typed, + // classified observations derived from described operations, with the legacy raw-print channel + // inventoried to zero. The bound row attaches here; the remaining rows attach as their + // execution contracts land. + active_small( + identity: "rn_YLLQF42HU2BZ52OXASZVS77F9", + id: "progress-observation", + owner: "ci-observation", + path: "docs/plans/progress-observation-design.md", + t: fields( + headline: "One trustworthy progress-observation channel", + boundary: "The progress-observation program: progress is emitted as classified observation rows derived from described operations; every place that printed progress directly is inventoried and either moved onto described operations or declared real program output; and scoped-run reach-throughs into hand-written code carry their seed-growth justification and dissolution trigger. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "Two competing status channels means neither can be trusted and formatting decisions scatter across the codebase; one typed channel is how a distant operator supervises an autonomous factory without reading every attempt.", + first_slice: "Read the rows in authored order: the raw-print retirement census, the scoped-run consumer, and the scoped-run seed-growth justification.", + red_control: "An observation row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted observation chain, every attached child's edge contribution readable against this intent, and no observation row whose walk ends anywhere else." + ) + ), + + // THE ROADMAP ACCEPTANCE-INTEGRITY PROGRAM LEVEL (review 5358022153 lane B, 2026-09-30). The + // roadmap-workspace-ux rows governing acceptance records are one program: what the roadmap has + // accepted stays accepted unless a named revocation withdraws it exactly, and the append-only + // carrier's replay projection is the only authority over that history. The bound row attaches + // here; the remaining rows attach as their execution contracts land. + active_small( + identity: "rn_PJKPB0BU0MRQ1ILRI1D4GOK1M", + id: "roadmap-acceptance-integrity", + owner: "roadmap-runtime", + path: "docs/plans/roadmap-workspace-ux-plan.md", + t: fields( + headline: "Roadmap acceptance records are durable and derived", + boundary: "The roadmap-runtime acceptance program: acceptance history is one append-only JSONL carrier; the receipt projection is a replay of that carrier and nothing else; and a record that was valid refuses to quietly become unaccepted -- any withdrawal names the exact item, the exact record being withdrawn, a reason, and what follows from it. This node is the program's decomposition level; the executable rows are its children.", + displaced_cost: "An accepted fact that goes missing puts an item back on the board as if it had never been done; the only detector was a person redoing the work, and that already happened once.", + first_slice: "Read the rows in authored order: the receipt-continuity comparison, then the workspace UX rows that consume the replayed projection.", + red_control: "A roadmap-runtime row claimed by a second project refuses as typed ambiguity; a row whose membership walk does not arrive here refuses unrooted; each child's edge names what that row's work adds to this program.", + out_of_scope: "No executable work lives on this node -- it is never dispatched and never carried by an execution contract -- and its bar does not replace the per-row tickets' own bars.", + handback: "The rooted acceptance-integrity chain, every attached child's edge contribution readable against this intent, and no roadmap-runtime row whose walk ends anywhere else." + ) + ), ], guarantee_ladder_nodes()) } diff --git a/dag/gunbc/roadmap/roadmap_checkpoint_plan.dag b/dag/gunbc/roadmap/roadmap_checkpoint_plan.dag new file mode 100644 index 00000000000..ae4bae74a60 --- /dev/null +++ b/dag/gunbc/roadmap/roadmap_checkpoint_plan.dag @@ -0,0 +1,221 @@ +module gunbc.roadmap.roadmap_checkpoint_plan + +import std.types { String, Int, List, NonEmptyStr } +import std.content_hash { content_hash_atom } +import gunbc.roadmap.roadmap_alignment { + AlignmentChainLink, + AlignmentResolvedChain, + AlignmentLevel, + RootPrinciplesLevel, + ProjectIntentLevel, + TaskContractLevel, + AlignmentVerdict, + Aligned, + Misaligned, + ExtremelyMisaligned, + CatastrophicallyMisaligned, + RoutedConflict, + AlignmentCadenceState, + AlignmentCheckpointIdentity, + alignment_checkpoint_identity_digest, + alignment_chain_fingerprint, + alignment_levels, +} + +// ------------------------------------------------------------------------------------------------ +// The checkpoint-probe plan (review 5358022153, lane C): the PURE checkpoint-subject/probe- +// obligation planner. This module makes NO model calls, NO filesystem or process effects, and NO +// verdicts -- its output is the obligation population a durable scheduler executes later. One +// checkpoint identity, one resolved chain, one cadence state in; the answer out is either the +// derived obligations or a typed refusal, never a partial population and never a silent []. +// ------------------------------------------------------------------------------------------------ + +// THE PER-LEVEL PROBE SUBJECT, homogeneous across the ladder: the principles level, one project +// intent level (carrying its chain link), or the task contract level (carrying its chain link). +// ProbePrinciples carries no payload because its contract text IS the chain's root DeclarationRef +// pointer; the obligation key preimage carries the ref's module and decl as strings, so the +// subject stays a pointer answerable to the live authority instead of a copied excerpt. +type AlignmentProbeLevel + = ProbePrinciples + | ProbeProject { link: AlignmentChainLink } + | ProbeTask { link: AlignmentChainLink } + +// THE OBLIGATION, one per level of the chain under one checkpoint. obligation_key is a content +// digest over (checkpoint identity digest + level identity), so re-deriving the plan after a +// crash under the same checkpoint identity yields byte-identical keys and the scheduler can key +// its durable records on them. outstanding_guidance carries the prior verdict's recorded payload +// for the level -- the direction, reason, or conflict the cadence state holds -- so a probe +// starts from what the last check actually found; an ALIGNED level, or one the state has not +// reached, carries no guidance. +type AlignmentProbeObligation { + checkpoint: AlignmentCheckpointIdentity + level: AlignmentProbeLevel + obligation_key: String + outstanding_guidance: List +} + +// THE PREIMAGE FIELD ENCODING IS LENGTH-PREFIXED, never separator-joined (review 5358022153, the +// same law as gunbc.harness.harness_prompt_context prompt_field_enc): a field containing the +// separator cannot manufacture another field sequence's preimage, and every semantic field -- +// checkpoint digest, level kind, level subjects -- participates. +fn probe_plan_field_enc(s: String) -> String { + join([to_string(value: string_length(s: s)), ":", s], "") +} + +// THE VERDICT'S RECORDED PAYLOAD, extracted for guidance: the named direction, the named reason, +// or the named conflict -- "" for ALIGNED, which records that nothing is outstanding. +fn probe_plan_verdict_detail(v: AlignmentVerdict) -> String { + match v { + Aligned => "" + Misaligned { direction } => direction + ExtremelyMisaligned { direction } => direction + CatastrophicallyMisaligned { reason } => reason + RoutedConflict { conflict } => conflict + } +} + +// THE STATE'S VERDICT AT ONE LEAF-DISTANCE INDEX, the convention alignment_state_interval and +// alignment_levels_due already read: index 0 is the task contract (leaf distance 0), counting up +// the ladder. A state authored before a level existed has no verdict there -- Absent reads as no +// recorded guidance, never a fabricated verdict. +fn probe_plan_verdict_at(items: List, index: Int) -> AlignmentVerdict? { + if index < 0 { + none + } else { + let found = fold( + items, + init: { seen: 0, hit: false, value: Aligned }, + f: fn(acc, item) { + if acc.hit { + acc + } else if acc.seen == index { + { seen: acc.seen + 1, hit: true, value: item } + } else { + { seen: acc.seen + 1, hit: false, value: Aligned } + } + }, + ) + if found.hit { + Present { value: found.value } + } else { + none + } + } +} + +// THE OUTSTANDING GUIDANCE for one level: the recorded verdict's payload when there is one and it +// names something to answer; [] for ALIGNED (nothing outstanding) and for a level the state has +// not reached. The empty list is the honest shape of "the last check found nothing": a probe that +// starts clean starts from no standing readings, never from a fabricated concern. +fn probe_plan_outstanding_guidance(verdict: AlignmentVerdict?) -> List { + match verdict { + Absent => { + let empty = [] as List + empty + } + Present { value: v } => { + let detail = probe_plan_verdict_detail(v: v) + if detail == "" { + let empty = [] as List + empty + } else { + [detail] + } + } + } +} + +// THE OBLIGATION KEY binds one obligation to its checkpoint identity and its level identity: the +// checkpoint identity digest is the digest of the lineage/epoch/ordinal/fingerprint/tip/subject +// preimage, and the level identity is the probe kind plus the level's named subject -- the link's +// node_id for a project or task level, the root pointer's module and decl (the ref as strings, +// per the ladder's pointer-not-excerpt law) for the principles level. The tag begins the preimage +// and every field is length-prefixed, so the atom's NonEmptyStr input is grounded and no field's +// content can collide with another field's. +fn probe_plan_obligation_key(checkpoint_digest: String, level: AlignmentProbeLevel, root_module: String, root_decl: String) -> String { + let kind = match level { + ProbePrinciples => "principles" + ProbeProject { link: _ } => "project" + ProbeTask { link: _ } => "task" + } + let subject_a = match level { + ProbePrinciples => root_module + ProbeProject { link } => link.node_id + ProbeTask { link } => link.node_id + } + let subject_b = match level { + ProbePrinciples => root_decl + ProbeProject { link: _ } => "" + ProbeTask { link: _ } => "" + } + content_hash_atom(value: join([ + "30:checkpoint-probe-obligation/v1", + probe_plan_field_enc(s: checkpoint_digest), + probe_plan_field_enc(s: kind), + probe_plan_field_enc(s: subject_a), + probe_plan_field_enc(s: subject_b), + ], "") as NonEmptyStr).digest as String +} + +// THE PLAN, typed because a moved chain must not derive obligations under a stale checkpoint: +// Derived carries the checkpoint's identity digest -- the idempotency root the scheduler keys on +// -- and the root-to-leaf obligation population; Refused carries the reason and NOTHING else. +// There is no empty plan and no partial population a consumer could misread as "nothing to probe". +type CheckpointProbePlan + = ProbePlanDerived { checkpoint_digest: String, obligations: List } + | ProbePlanRefused { detail: String } + +// THE DERIVATION, pure and total over its three inputs. The fingerprint gate comes FIRST: the +// checkpoint names the chain fingerprint it was minted under, and if the resolved chain's current +// fingerprint has moved -- a rewired edge, a retitled row, any carried-field edit -- the plan +// refuses rather than minting obligations a stale checkpoint would later execute. The gate +// passing, every level of alignment_levels(chain), ROOT TO LEAF in the ladder's reading order, +// becomes one obligation: RootPrinciplesLevel -> ProbePrinciples, ProjectIntentLevel -> +// ProbeProject, TaskContractLevel -> ProbeTask, each keyed over the checkpoint digest and its +// level identity, each carrying the cadence state's recorded verdict payload at the level's LEAF +// DISTANCE ((count(levels) - 1) - position, the convention alignment_levels_due reads: index 0 +// is the task contract). The derivation is deterministic in (checkpoint, chain, state), which is +// exactly what makes the keys re-derivable after a crash. +fn checkpoint_probe_obligations( + checkpoint: AlignmentCheckpointIdentity, + chain: AlignmentResolvedChain, + state: AlignmentCadenceState, +) -> CheckpointProbePlan { + let current_fingerprint = alignment_chain_fingerprint(chain: chain) + if checkpoint.chain_fingerprint != current_fingerprint { + ProbePlanRefused { + detail: join([ + "checkpoint chain_fingerprint ", checkpoint.chain_fingerprint, + " does not match the resolved chain's current fingerprint ", current_fingerprint, + "; the chain moved after the checkpoint was minted, and a moved chain must not derive obligations under a stale checkpoint", + ], ""), + } + } else { + let levels = alignment_levels(chain: chain) + let total = count(levels) + let checkpoint_digest = alignment_checkpoint_identity_digest(id: checkpoint) + let root_module = chain.root.module_path as String + let root_decl = chain.root.decl_name as String + let derived = fold( + levels, + init: { obligations: [] as List, position: 0 }, + f: fn(acc, level) { + let probe_level = match level { + RootPrinciplesLevel => ProbePrinciples + ProjectIntentLevel { link } => ProbeProject { link: link } + TaskContractLevel { link } => ProbeTask { link: link } + } + let leaf_distance = total - 1 - acc.position + let verdict = probe_plan_verdict_at(items: state.last_verdicts, index: leaf_distance) + let obligation = AlignmentProbeObligation { + checkpoint: checkpoint, + level: probe_level, + obligation_key: probe_plan_obligation_key(checkpoint_digest: checkpoint_digest, level: probe_level, root_module: root_module, root_decl: root_decl), + outstanding_guidance: probe_plan_outstanding_guidance(verdict: verdict), + } + { obligations: concat(acc.obligations, [obligation]), position: acc.position + 1 } + }, + ) + ProbePlanDerived { checkpoint_digest: checkpoint_digest, obligations: derived.obligations } + } +} diff --git a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag index 051e54cfb70..0555a2ea135 100644 --- a/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag +++ b/dag/gunbc/roadmap/roadmap_dispatch_actuator.dag @@ -39,7 +39,7 @@ import gunbc.roadmap.roadmap_attempt_continuation { } import gunbc.roadmap.roadmap_alignment { AlignmentResolution, AlignmentReady, AlignmentUnresolved, alignment_refusal_reason, - alignment_resolve, alignment_base_cadence_state, + alignment_resolve, alignment_base_cadence_state, alignment_chain_fingerprint, } import gunbc.roadmap.roadmap_supervisor { supervisor_verdict_basename } import gunbc.dispatch_pipe_pane_emit { @@ -794,6 +794,21 @@ fn dispatch_attempt_events_path_for_instance( ], "") } +// THE WIRE LOG DIRECTORY (harness-conversation-log-forking): per-round request/response bytes +// persist beside the provider events file under ".wire/"; provisioning it here is what +// makes the harness's round writes land instead of degrading to turn.wire_log_unwritable. +fn dispatch_attempt_wire_dir_for_instance( + instance: HostDashboardInstance, + node_id: RoadmapNodeId, + attempt_key: String, +) -> String { + join([dispatch_attempt_events_path_for_instance( + instance: instance, + node_id: node_id, + attempt_key: attempt_key, + ), ".wire/"], "") +} + fn dispatch_attempt_key_path_for_instance( instance: HostDashboardInstance, node_id: RoadmapNodeId, @@ -1922,6 +1937,7 @@ fn gunbc_harness_spawn_argv( request_scratch_path: String, events_path: String, attempt_identity: String, + alignment_chain_fingerprint: String, ) -> List { [ gunbc_program, @@ -1938,6 +1954,7 @@ fn gunbc_harness_spawn_argv( "--arg", join(["repo_root=", repo_root], ""), "--arg", join(["gunbc_program=", gunbc_program], ""), "--arg", join(["attempt_identity=", attempt_identity], ""), + "--arg", join(["alignment_chain_fingerprint=", alignment_chain_fingerprint], ""), ] } @@ -1994,6 +2011,7 @@ fn dispatch_provider_inner_argv_for_instance( attempt_key: String, worktree_path: String, brief: String, + alignment_chain_fingerprint: String, ) -> DispatchProviderInnerArgv { match provider { ClaudeCodeProvider => @@ -2059,6 +2077,7 @@ fn dispatch_provider_inner_argv_for_instance( attempt_key: attempt_key, ), attempt_identity: dispatch_worker_attempt_identity(node_id: node_id as String, attempt_key: attempt_key), + alignment_chain_fingerprint: alignment_chain_fingerprint, ), } } @@ -2172,6 +2191,11 @@ fn attempt_state_prepare_argv_for_instance( node_id: node_id, attempt_key: attempt_key, ), + dispatch_attempt_wire_dir_for_instance( + instance: instance, + node_id: node_id, + attempt_key: attempt_key, + ), ], ) } @@ -2748,7 +2772,7 @@ fn dispatch_spawn_commands_for_resolved_instance( step: "alignment" as NonEmptyStr, detail: alignment_refusal_reason(cause: cause), } - AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => + AlignmentReady { chain, due_levels: _, rendered_prompt: _ } => match node_dispatch_destination(node: node) { DispatchDestinationMissing { reason } => DispatchSpawnRefused { @@ -2784,6 +2808,7 @@ fn dispatch_spawn_commands_for_resolved_instance( attempt_key: attempt_key, worktree_path: worktree_path, brief: brief, + alignment_chain_fingerprint: alignment_chain_fingerprint(chain: chain), ) { CursorProviderSpawnUnwired { reason } => DispatchSpawnRefused { diff --git a/dag/gunbc/roadmap/roadmap_issue_sort.dag b/dag/gunbc/roadmap/roadmap_issue_sort.dag index 1c9cb22cef4..aa582ac7821 100644 --- a/dag/gunbc/roadmap/roadmap_issue_sort.dag +++ b/dag/gunbc/roadmap/roadmap_issue_sort.dag @@ -8,6 +8,11 @@ import extdeps.languages.typescript.program { // 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. +// +// THE ISSUE LINK NAVIGATES; THE ROW FOCUSES (owner ruling): clicking the title link follows the +// link to the issue page, and the project's topological strip is a separate trigger -- the +// "topological sort" chip, or focusing a row by clicking its body (not its title) or pressing +// Enter/Space on it. Intercepting the link click to open the strip instead stole navigation. 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: [ @@ -166,7 +171,10 @@ ts_if(cond: ts_binop(left: ts_binop(left: ts_binop(left: ts_binop(left: ts_membe 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_if(cond: ts_ident(name: "link"), then: ts_block(stmts: [ +ts_return(expr: none) +]), else_stmt: none), +ts_if(cond: 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) diff --git a/dag/gunbc/roadmap/roadmap_nesting.dag b/dag/gunbc/roadmap/roadmap_nesting.dag index 1cc5567ee0a..07e44858c4b 100644 --- a/dag/gunbc/roadmap/roadmap_nesting.dag +++ b/dag/gunbc/roadmap/roadmap_nesting.dag @@ -4,22 +4,84 @@ 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 } +// +// THE EDGE CARRIES THE ALIGNMENT ARGUMENT (review 5354823380). contribution says why the member's +// work advances THIS project -- a membership list without it is a list of titles, which no +// checkpoint can be read against. It belongs to the relationship, not to either node: the same +// member under a different project would play a different role. +type RoadmapMembership { member: String, project: String, contribution: String } + +// THE ADMITTED ROOTS. A membership walk may end parentless only at one of these -- every other +// standalone end means the chain never reached the top of a functional decomposition, and the +// alignment projection refuses rather than calling an arbitrary node the root +// (gunbc.roadmap.roadmap_alignment AlignmentChainUnrooted). DESIGN.md is NOT here: it is the +// principles authority orthogonal to the hierarchy, reached as the chain's terminal pointer, not +// as a project level. +fn declared_roadmap_roots() -> List { + ["gunbc-project-root"] +} // 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. +// +// The harness lineage (review 5354823380): the checkpoint task serves recursive alignment, +// recursive alignment serves the work lifecycle, and the lifecycle serves the root project. +// +// THE PROGRAM ATTACHMENTS (review 5358022153 lane B, 2026-09-30): every dispatchable node (one +// carrying an ExecutionContract) must have a membership walk terminating at the admitted root. +// Eighteen bound rows had no honest existing parent: thirteen ride seven new program-level nodes +// authored in roadmap_authority (the native-SCM program, the namespace lane, the v1 deletion +// program, the v2 emitter production lane, CI end-to-end execution, progress observation, and +// roadmap acceptance integrity), and five completed belt-era rows ride the existing +// harness-work-lifecycle. Each edge's contribution states what that row's work adds to its +// program; each program's root edge states why the program serves the factory. The remaining +// unbound population attaches as its execution contracts land; extended-admission-cardinality +// stays unrooted on purpose -- it is the named refusal subject of +// roadmap_alignment_witness_test's unrooted controls. 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" } + RoadmapMembership { member: "shell-dag-cron-entry-line-builder", project: "shell-typed-invocation", contribution: "Shows the pattern on the smallest surface: one rendered line derived from typed intent, proving callers can name operations instead of shell text." }, + RoadmapMembership { member: "shell-dag-floor-peak-calibration-rows", project: "shell-typed-invocation", contribution: "Removes the echo-row calibration shell so the project's claim covers observation rows as well as invocations." }, + RoadmapMembership { member: "shell-dag-ci-fmt-gate-line", project: "shell-typed-invocation", contribution: "Moves the CI format gate's line-building onto described operations, extending the pattern into the CI surfaces." }, + RoadmapMembership { member: "shell-dag-ci-deploy-invoke-prelude", project: "shell-typed-invocation", contribution: "Moves the deploy invocation prelude onto described operations, so the riskiest CI call is checkable before it runs." }, + RoadmapMembership { member: "shell-dag-ci-heal-regen-invoke", project: "shell-typed-invocation", contribution: "Moves the heal/regen invocation onto described operations, closing the CI invocation pair with the deploy prelude." }, + RoadmapMembership { member: "shell-dag-compile-pool-slice-install", project: "shell-typed-invocation", contribution: "Describes the compile-pool slice installation instead of scripting it, so the factory's own build plumbing follows the pattern." }, + RoadmapMembership { member: "shell-dag-slot-population-census-read", project: "shell-typed-invocation", contribution: "Describes the slot census read, replacing a shell probe whose output shape nothing could check." }, + RoadmapMembership { member: "shell-dag-codex-app-server-trip-script", project: "shell-typed-invocation", contribution: "Describes the Codex app-server trip script, carrying the pattern into provider tooling." }, + RoadmapMembership { member: "shell-dag-live-deploy-install-owned", project: "shell-typed-invocation", contribution: "Describes the owned live-deploy install, so deployment of our own service is as checkable as the CI calls." }, + RoadmapMembership { member: "shell-dag-live-deploy-restart-tailscale", project: "shell-typed-invocation", contribution: "Describes the Tailscale restart step, completing the live-deploy pair." }, + RoadmapMembership { member: "shell-typed-invocation", project: "gunbc-project-root", contribution: "Described commands are the factory's own dogfood: every invocation the harness issues becomes checkable before it runs." }, + RoadmapMembership { member: "harness-alignment-checkpoint-resume", project: "harness-recursive-alignment", contribution: "The checkpoint turns an arbitrary terminal counter into the durable mechanism that enforces recursive alignment." }, + RoadmapMembership { member: "harness-conversation-log-forking", project: "harness-recursive-alignment", contribution: "The lineage-owned conversation log is what makes resume and probe forks possible at all: without it every epoch is a cold start and every fork point is imaginary." }, + RoadmapMembership { member: "harness-checkpoint-subject-planner", project: "harness-recursive-alignment", contribution: "The pure planner turns a checkpoint into durable, idempotent obligations, so the parallel probe fan-out is a schedule over derived facts rather than an in-loop improvisation." }, + RoadmapMembership { member: "harness-prompt-context-model", project: "harness-work-lifecycle", contribution: "The lifecycle's mandatory steps are only workflow-owned if what the agent is TOLD is also derived and revisioned; modeled prompt inputs make prompt contradiction a dispatch-time refusal instead of a runtime prayer." }, + RoadmapMembership { member: "harness-recursive-alignment", project: "harness-work-lifecycle", contribution: "Recursive alignment permits autonomous continuation without silently trading longer execution for project drift." }, + RoadmapMembership { member: "harness-work-lifecycle", project: "gunbc-project-root", contribution: "The lifecycle makes the software factory autonomous without relying on an agent remembering mandatory obligations." }, + RoadmapMembership { member: "harness-thinking-and-write-briefs", project: "harness-work-lifecycle", contribution: "Engine-direct runs on finishable write briefs are the execution shape the lifecycle's autonomy assumes; this signed row proved the shape on real runs, one mechanical brief green on the first try." }, + RoadmapMembership { member: "roadmap-verify-to-publish-phase", project: "harness-work-lifecycle", contribution: "The belt committing the attempt worktree, verifying the detached head, and opening the pull request itself is the lifecycle's publish phase made mechanical: the loop closes between verdict and PR without an operator." }, + RoadmapMembership { member: "roadmap-continuation-turn-label", project: "harness-work-lifecycle", contribution: "One function naming turn N of M is what lets the lifecycle's resume briefs and its dashboard rows agree without either joining the pieces itself -- the label authority the checkpoint's resumption reads." }, + RoadmapMembership { member: "roadmap-round-strip-pulse-follows-the-worker", project: "harness-work-lifecycle", contribution: "The strip pulsing only while the worker process is actually running is the lifecycle's aliveness evidence on the page: a finished attempt stops claiming to be alive while its obligations remain open." }, + RoadmapMembership { member: "roadmap-round-strip-percolator", project: "harness-work-lifecycle", contribution: "The time-proportional round replay makes an attempt's whole run readable at a glance however many rounds it took -- the lifecycle's history surfaced as a fixed-length strip rather than an unbounded one." }, + RoadmapMembership { member: "2-scm-p1-proof-kernel", project: "dag-scm-program", contribution: "Proving one bounded reconciliation rule works for directly authored changes -- invariants and certificates contributed as data, every refusal typed and counted -- is the program's safety core: the landing spine and every later write realization compose transformations through this fold instead of through pairwise cases." }, + RoadmapMembership { member: "dag-scm-program", project: "gunbc-project-root", contribution: "The factory's own development is its first customer: authoring, proposals, integration and history decided natively -- with Git demoted to a compatibility projection -- is the product the root's self-hosting claim is built on." }, + RoadmapMembership { member: "namespace-structural-observations", project: "namespace-unique-on-chain", contribution: "Observing all four in-view cases on one ordinary compiled file -- rather than on hand-built examples -- is the evidence base the program's derived closure and canonical changeover stand on." }, + RoadmapMembership { member: "namespace-reference-derived-closure", project: "namespace-unique-on-chain", contribution: "Deriving what each file depends on from the binding answers retires the hand-written dependency statements, which is the program's core claim: a file's dependencies are a consequence of what its names resolve to, not a second thing to maintain." }, + RoadmapMembership { member: "namespace-unique-on-chain", project: "gunbc-project-root", contribution: "A proof-carrying factory cannot carry proofs about programs whose names mean more than one thing; one canonical binding decided from where each name is written is the correctness surface every other guarantee reads through." }, + RoadmapMembership { member: "v1-interpreter-primitive-roster", project: "v1-deletion-program", contribution: "Making the interpreter's primitive surface enumerable and derived from its dispatch is the denominator deletion must show empty: nothing can be proven unneeded if it cannot be listed." }, + RoadmapMembership { member: "v1-interpreter-primitive-dispatch-authority", project: "v1-deletion-program", contribution: "Moving the primitive denominator from Rust into the model stops the roster growing behind every consumer's back, so the deletion frontier is a derived population rather than a judgment call." }, + RoadmapMembership { member: "v1-rust-source-observation", project: "v1-deletion-program", contribution: "Reading the hand-written population from the repository at a known revision -- and refusing a failed read -- is what makes the deletion frontier a count from observation instead of from inspection." }, + RoadmapMembership { member: "v1-test-migration", project: "v1-deletion-program", contribution: "Matching every deleted old test to coverage that would actually catch its bug keeps deletion from silently dropping the protection the old tests existed for." }, + RoadmapMembership { member: "v1-seed-honesty-decision", project: "v1-deletion-program", contribution: "Making the self-verify check able to fail and recording whether independent rebuild must precede removal is the trust ruling the deletion plan's remaining cuts are gated on." }, + RoadmapMembership { member: "v1-deletion-program", project: "gunbc-project-root", contribution: "The factory's self-host claim is a claim that the old compiler it replaces no longer decides anything; this program is the derived frontier and recorded ruling that makes 'the old code is gone' evidence rather than assertion." }, + RoadmapMembership { member: "v2-emitter-direct-rust-door", project: "v2-emitter-production", contribution: "Carrying the real inferred tree through translation and writing Rust with the old generator deciding nothing is the capability this lane exists to name: the new compiler writes the source the factory is built from." }, + RoadmapMembership { member: "v2-emitter-producer-provenance", project: "v2-emitter-production", contribution: "Recording which generator made every result as a fact that cannot be claimed without its receipt is the lane's progress measure: the transition from old generator to new is countable, not asserted." }, + RoadmapMembership { member: "v2-emitter-production", project: "gunbc-project-root", contribution: "Self-hosting is the root's identity; an emitter that writes the compiler's own Rust and proves which generator made every result is the measurable half of that claim." }, + RoadmapMembership { member: "ci-executed-coverage-receipt", project: "ci-end-to-end-execution", contribution: "Making one required check carry the population it actually executed -- and refuse a verdict on an empty one -- is the program's core honesty property: a verdict names the work that produced it." }, + RoadmapMembership { member: "ci-end-to-end-execution", project: "gunbc-project-root", contribution: "The factory's checks are its proof machinery; a CI verdict that names the population it executed is admissible as evidence everywhere else in the tree, and inadmissible without it." }, + RoadmapMembership { member: "observation-raw-print-retirement", project: "progress-observation", contribution: "Driving the raw-print inventory to zero unclassified completes the move to a single typed observation channel, so progress claims stop competing with an untyped one nobody can check." }, + RoadmapMembership { member: "progress-observation", project: "gunbc-project-root", contribution: "An autonomous factory is supervised through what it reports; one classified observation channel is how a distant operator trusts progress claims without reading every attempt." }, + RoadmapMembership { member: "roadmap-receipt-continuity", project: "roadmap-acceptance-integrity", contribution: "Refusing any acceptance record that vanishes without a named revocation is the program's central law applied to one real change: what the roadmap accepted stays accepted until a withdrawal names it exactly." }, + RoadmapMembership { member: "roadmap-acceptance-integrity", project: "gunbc-project-root", contribution: "The factory coordinates work through the roadmap; acceptance records that cannot quietly revert are what make 'done' a durable fact the whole hierarchy reads." }, ] } diff --git a/dag/gunbc/roadmap/roadmap_page.dag b/dag/gunbc/roadmap/roadmap_page.dag index 080d8aaf3b1..bbb6c1526bb 100644 --- a/dag/gunbc/roadmap/roadmap_page.dag +++ b/dag/gunbc/roadmap/roadmap_page.dag @@ -31,10 +31,10 @@ import gunbc.roadmap_style { roadmap_css, round_fill_delay_var, round_fill_durat import gunbc.fleet_capacity_panel { fleet_capacity_panel } import gunbc.roadmap_model { RoadmapNode, RoadmapEdge, RoadmapNodeId, - RoadmapParent, TopLevel, UnderParent, roadmap_primary_parent, is_top_level, + RoadmapParent, TopLevel, UnderParent, roadmap_primary_parent, roadmap_parent_ids, is_top_level, SectionElement, Prose, Group, SectionGroup, } -import gunbc.roadmap_status { line_ticket } +import gunbc.roadmap_status { line_ticket, TicketLine } import gunbc.roadmap_document { RoadmapDocument, RoadmapSection } import gunbc.fleet_hardware_standing_panel { fleet_hardware_standing_panel } import gunbc.roadmap_altitude { section_counts, section_counts_wire, statuses_all_routine, SectionCounts } @@ -46,9 +46,15 @@ import gunbc.roadmap_authority { frontier_sets_from_live_history, FrontierSetsProjected, FrontierSetsProjectionRefused, declared_roadmap_nodes, + declared_roadmap_edges, ClosingContractTaskPair, closing_contract_task_pairs_for_accepted, closing_contract_family_claim, } -import gunbc.roadmap_spawner { spawn_frontier, SpawnFrontier, doc_all_nodes, doc_all_edges } +import gunbc.roadmap_spawner { spawn_frontier, SpawnFrontier, doc_all_nodes, doc_all_edges, node_title } +import gunbc.roadmap.roadmap_alignment { + AlignmentResolution, AlignmentReady, AlignmentUnresolved, + AlignmentLevel, RootPrinciplesLevel, ProjectIntentLevel, TaskContractLevel, + alignment_levels, alignment_resolve, alignment_base_cadence_state, alignment_refusal_reason, +} import gunbc.roadmap_component { activity_round_live_class, activity_round_remaining_class, round_strip_playback_ms } import gunbc.roadmap_component { roadmap_dispatch_asset_path, sound_toggle_class, theme_toggle_class, @@ -2290,6 +2296,84 @@ fn issue_head_chip_nodes(view: WorkRowView) -> List { status_chip_nodes(view: view) } +// THE PROJECT PATH BREADCRUMB: one navigation answer per issue, rendered directly above the page +// body. The ONLY derivation named is alignment_resolve -- the same resolution the dispatch gate +// consumes -- never a second path answer folded out of the raw memberships. The ready arm renders +// the chain's levels ROOT-TO-LEAF: each project level links to its /project/ suite, the +// task level is the current page and renders as plain text (no self-link), and the +// root-principles pointer is skipped (it is a pointer to gunbc.design_document, not a page). The +// unresolved arm renders the typed refusal itself -- a partial path is never dressed as a complete +// one. +type IssueBreadcrumbCrumb + = IssueBreadcrumbProject { node_id: String, purpose: String, boundary: String } + | IssueBreadcrumbTask { node_id: String, purpose: String, boundary: String } + +type IssueBreadcrumb + = IssueBreadcrumbPath { crumbs: List } + | IssueBreadcrumbRefused { reason: String } + +// THE CRUMBS from the chain's levels: the root-principles level contributes no crumb (a pointer, +// not a page); every other level contributes one crumb carrying its purpose and boundary. +fn issue_breadcrumb_crumbs(levels: List) -> List { + fold(levels, init: [] as List, f: fn(acc, level) { + match level { + RootPrinciplesLevel => acc + ProjectIntentLevel { link } => concat(acc, [IssueBreadcrumbProject { node_id: link.node_id, purpose: link.purpose, boundary: link.boundary }]) + TaskContractLevel { link } => concat(acc, [IssueBreadcrumbTask { node_id: link.node_id, purpose: link.purpose, boundary: link.boundary }]) + } + }) +} + +fn issue_breadcrumb_of(resolution: AlignmentResolution) -> IssueBreadcrumb { + match resolution { + AlignmentReady { chain, due_levels: _, rendered_prompt: _ } => + IssueBreadcrumbPath { crumbs: issue_breadcrumb_crumbs(levels: alignment_levels(chain: chain)) } + AlignmentUnresolved { cause } => + IssueBreadcrumbRefused { reason: alignment_refusal_reason(cause: cause) } + } +} + +fn issue_breadcrumb_crumb_node(crumb: IssueBreadcrumbCrumb) -> Fragment { + match crumb { + IssueBreadcrumbProject { node_id, purpose, boundary } => + el_attrs("a", [ + attr(name: "href", value: join(["/project/", node_id], "")), + attr(name: "title", value: boundary), + ], [ txt(purpose) ]) + IssueBreadcrumbTask { node_id: _, purpose, boundary } => + el_attrs("span", [ + attr(name: "class", value: "issue-breadcrumb-here"), + attr(name: "title", value: boundary), + ], [ txt(purpose) ]) + } +} + +// THE CRUMB LIST'S SEPARATOR LAW: " › " BETWEEN crumbs only -- never leading, never trailing -- +// so the single-crumb path and the refusal arm emit exactly their own text and nothing else. +fn issue_breadcrumb_nodes(breadcrumb: IssueBreadcrumb) -> List { + match breadcrumb { + IssueBreadcrumbPath { crumbs } => { + let spaced = fold(crumbs, init: { out: [] as List, first: true }, f: fn(acc, crumb) { + { + out: if acc.first { + concat(acc.out, [issue_breadcrumb_crumb_node(crumb: crumb)]) + } else { + concat(acc.out, [txt(" › "), issue_breadcrumb_crumb_node(crumb: crumb)]) + }, + first: false, + } + }) + spaced.out + } + IssueBreadcrumbRefused { reason } => + [ el_class("span", "issue-breadcrumb-refused", [ txt(join(["project path unavailable: ", reason], "")) ]) ] + } +} + +fn issue_alignment_breadcrumb_node(resolution: AlignmentResolution) -> Fragment { + el_class("nav", "issue-breadcrumb", issue_breadcrumb_nodes(breadcrumb: issue_breadcrumb_of(resolution: resolution))) +} + 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" ), @@ -2306,6 +2390,7 @@ fn issue_detail_page(view: WorkRowView, panel: IssueLinkPanel, observation: Obse issue_tab_chip(label: "work", tab: "work"), issue_tab_chip(label: "history", tab: "history"), ]), + issue_alignment_breadcrumb_node(resolution: alignment_resolve(node_id: view.node_id, turns_elapsed: 1, state: alignment_base_cadence_state())), 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, command_context: command_context)), @@ -2579,6 +2664,146 @@ fn issue_detail_not_found_page(node_id: String, viewer: String) -> Fragment { } } +// THE PROJECT SUITE PAGE (GET /project/): ONE project's direct membership listing -- +// the members declared under it in gunbc.roadmap_nesting, in authored order as the base order, +// then Kahn-layered by the declared dependency edges restricted to the member set: a member whose +// in-set prerequisites include another member sorts AFTER that member, and members with no +// in-set prerequisites keep their authored order at the front. The heading is the project row's +// own ticket headline; an absent row or a non-ticket row renders the not-found document, the same +// grain as the issue detail route. +type ProjectSuiteRow { node_id: String, title: String } + +// THE HEADING'S TICKET PROJECTION: exactly one declared row, and that row a ticket -- anything +// else is a not-found, because a suite whose heading cannot be read against the row's own brief +// would be a listing no check can read against. +fn project_suite_headline(nodes: List, node_id: String) -> String? { + match (filter(nodes, n => (n.node as String) == node_id)).first() { + Absent => none + Present { value: rn } => + match rn.line { + TicketLine { done: _, ticket } => Present { value: ticket.headline } + _ => none + } + } +} + +// THE DIRECT MEMBERS, in authored order: the memberships whose project field names this suite -- +// nothing from another project, nothing nested one level deeper. +fn project_suite_member_ids(memberships: List, project: String) -> List { + map(filter(memberships, m => m.project == project), m => m.member) +} + +// A MEMBER'S IN-SET PREREQUISITES: the parent ends of its declared dependency edges (the +// roadmap_parent_ids read) restricted to members of this suite -- an edge whose parent stands +// outside the member set constrains nothing here. +fn project_suite_in_set_prereqs(edges: List, member: String, members: List) -> List { + filter(roadmap_parent_ids(edges: edges, node: member as NonEmptyStr as RoadmapNodeId), p => any(members, m => m == (p as String))) +} + +// ONE KAHN LAYER: every not-yet-emitted member whose in-set prerequisites are all already emitted, +// in authored order. A STABLE sort by construction: filter preserves the authored order the +// membership relation supplies. +fn project_suite_next_layer(members: List, edges: List, emitted: List) -> List { + filter(members, m => !any(emitted, e => e == m) + && all(project_suite_in_set_prereqs(edges: edges, member: m, members: members), p => any(emitted, e => e == p))) +} + +// A CYCLE STOPS PROGRESS: when no not-yet-emitted member can advance (a dependency cycle inside +// the member set), the remainder emits in authored order -- the listing degrades to declaration +// order rather than refusing the page over a fixture cycle. The fuel bound is count(members): +// every advancing layer emits at least one member, so a cycle is the only way to exhaust it. +fn project_suite_order_step(members: List, edges: List, emitted: List, fuel: Int) -> List { + if fuel <= 0 { + concat(emitted, filter(members, m => !any(emitted, e => e == m))) + } else { + let layer = project_suite_next_layer(members: members, edges: edges, emitted: emitted) + if count(layer) == 0 { + concat(emitted, filter(members, m => !any(emitted, e => e == m))) + } else { + project_suite_order_step(members: members, edges: edges, emitted: concat(emitted, layer), fuel: fuel - 1) + } + } +} + +fn project_suite_ordered_members(members: List, edges: List) -> List { + project_suite_order_step(members: members, edges: edges, emitted: [], fuel: count(members)) +} + +// THE ROW'S TITLE: the node row's own title when one exists, the raw id when none does -- an +// undeclared member still lists, honestly labeled. +fn project_suite_member_title(nodes: List, member: String) -> String { + match (filter(nodes, n => (n.node as String) == member)).first() { + Absent => member + Present { value: rn } => node_title(item: rn.line) + } +} + +// THE LISTING, one row per direct member, topologically ordered. +fn project_suite_rows(memberships: List, edges: List, nodes: List, project: String) -> List { + let ordered = project_suite_ordered_members(members: project_suite_member_ids(memberships: memberships, project: project), edges: edges) + map(ordered, m => ProjectSuiteRow { node_id: m, title: project_suite_member_title(nodes: nodes, member: m) }) +} + +fn project_suite_row_node(row: ProjectSuiteRow) -> Fragment { + el_class("li", "project-suite-member", [ + el_attrs("a", [ + attr(name: "href", value: join(["/issue/", row.node_id], "")), + attr(name: "title", value: row.node_id), + ], [ txt(row.title) ]), + ]) +} + +fn project_suite_page_launch(headline: String, node_id: String, rows: List, viewer: String) -> Fragment { + Element { + tag: "html", + attributes: [ attr(name: "lang", value: "en" ) ], + children: [ + el("head", [ + Element { tag: "meta", attributes: [ attr(name: "charset", value: "utf-8" ) ], children: [] }, + Element { tag: "meta", attributes: [ attr(name: "name", value: "viewport" ), attr(name: "content", value: "width=device-width, initial-scale=1" ) ], children: [] }, + el("style", [ txt(roadmap_css()) ]), + el("title", [ txt(join(["gunbc — project ", node_id], "")) ]), + ]), + el("body", [ + roadmap_header(page: "project"), + el_class("main", "project-suite", [ + el_class("div", "issue-detail-head", [ + el_class("span", "issue-id", [ txt(node_id) ]), + el_class("h1", "issue-detail-title", [ txt(headline) ]), + ]), + if count(rows) == 0 { + el_class("p", "project-suite-empty", [ txt("no direct members declared") ]) + } else { + el_class("ul", "project-suite-members", map(rows, r => project_suite_row_node(row: r))) + }, + ]), + ]), + ], + } +} + +// THE NOT-FOUND DOCUMENT, the issue route's grain: an absent row or a non-ticket row is a missing +// project, said in one sentence. +fn project_suite_not_found_page(node_id: String, viewer: String) -> Fragment { + Element { + tag: "html", + attributes: [ attr(name: "lang", value: "en" ) ], + children: [ + el("head", [ + Element { tag: "meta", attributes: [ attr(name: "charset", value: "utf-8" ) ], children: [] }, + el("style", [ txt(roadmap_css()) ]), + el("title", [ txt("gunbc — project not found") ]), + ]), + el("body", [ + roadmap_header(page: "project"), + el_class("main", "project-suite", [ + el_class("p", "issue-not-found", [ txt(join(["no project named ", node_id, " is declared as a ticket in the roadmap authority"], "")) ]), + ]), + ]), + ], + } +} + // THE WORKSPACE RENDERS THE TABLE ALONE (owner ruling 2026-09-25: the tree/bands views and their // forest container retire from the served page — see the surface note above issue_owner_value). // The band derivation still runs one level down: DailyWorkspaceView's active/ready/upcoming/done/ diff --git a/dag/gunbc/roadmap/roadmap_project.dag b/dag/gunbc/roadmap/roadmap_project.dag index 9c99e14d79a..873556719c5 100644 --- a/dag/gunbc/roadmap/roadmap_project.dag +++ b/dag/gunbc/roadmap/roadmap_project.dag @@ -10,10 +10,24 @@ type ProjectMembership | MembershipMember { arc: String } | MembershipAmbiguous { arcs: List } +// Duplicate rows for the SAME (member, project) pair are not a fork -- the ownership question is +// which PROJECT owns the node, so arcs dedupe by project; whether the duplicated rows agree on the +// contribution argument is the contribution read's question (gunbc.roadmap.roadmap_alignment +// alignment_membership_contribution_read), never this one's. +fn project_distinct_strings(items: List) -> List { + fold(items, init: [] as List, f: fn(acc, item) { + if any(acc, x => x == item) { + acc + } else { + concat(acc, [item]) + } + }) +} + fn node_project_membership(node_id: String, memberships: List) -> ProjectMembership { - let arcs = memberships + let arcs = project_distinct_strings(items: memberships |> filter(e => e.member == node_id) - |> map(e => e.project) + |> map(e => e.project)) if count(arcs) == 0 { MembershipStandalone } else if count(arcs) == 1 { diff --git a/dag/gunbc/roadmap/roadmap_provider_events.dag b/dag/gunbc/roadmap/roadmap_provider_events.dag index ff87186ef13..48d8e680661 100644 --- a/dag/gunbc/roadmap/roadmap_provider_events.dag +++ b/dag/gunbc/roadmap/roadmap_provider_events.dag @@ -27,11 +27,14 @@ type CodexItemActivity | CodexReporting | CodexUnclassifiedItem -// THE GUNBC HARNESS INTERLEAVES ONE EVENT OF ITS OWN in the Codex-shaped stream: round.usage, the -// per-round token and clock accounting the dashboard's round strip reads (gunbc.harness.harness_throughput). -// The phase fold leaves the provider state unchanged on it; before this arm the whole stream was -// refused at that line as an unknown event type, so a live harness attempt rendered as -// "Agent evidence refused" with no strip (first belt-dispatched attempt, 2026-09-04). +// THE GUNBC HARNESS INTERLEAVES EVENTS OF ITS OWN in the Codex-shaped stream: round.usage, the +// per-round token and clock accounting the dashboard's round strip reads (gunbc.harness.harness_throughput); +// turn.alignment_checkpoint_due, the epoch-boundary announcement a chain-carrying turn ends on; +// and turn.wire_log_unwritable, the non-terminal notice that a round's durable wire-log write +// refused (gunbc.harness.harness_turn). The phase fold leaves the provider state unchanged on the +// non-terminal pair; before these arms the whole stream was refused at such a line as an unknown +// event type, so a live harness attempt rendered as "Agent evidence refused" with no strip +// (first belt-dispatched attempt, 2026-09-04). type CodexProviderEventKind = CodexThreadStarted | CodexTurnStarted @@ -42,6 +45,8 @@ type CodexProviderEventKind | CodexError | HarnessRoundUsage | HarnessBudgetExhausted + | HarnessAlignmentCheckpointDue + | HarnessWireLogUnwritable type CodexProviderEvent { line_number: Int @@ -69,7 +74,9 @@ type CodexParseFold { } // BUDGET SPENT IS TERMINAL AND IS NOT FAILED. The harness ends a turn when its step budget is spent -// (gunbc.harness.harness_turn turn.budget_exhausted); the work those rounds produced is in the +// (gunbc.harness.harness_turn turn.budget_exhausted) or when a chain-carrying epoch announces its +// alignment checkpoint (turn.alignment_checkpoint_due -- adjudication is the workflow's, and it is +// not a pronouncement on the work); the work those rounds produced is in the // worktree and the belt commits and verifies it exactly as it does a completed turn. Folding it into // ProviderFailed put "Agent failed" beside "Verification passed" on one row (attempt 7f08b76ceb8f4424, // 2026-09-04), a pair the reader cannot reconcile because it is false: nothing failed. The verdict is @@ -155,7 +162,9 @@ fn codex_provider_event_projection_args(path: FilePath) -> List { data codex_projection_filter_scaffold_note: String = "DECLARED SCAFFOLD (review 44058, DESIGN 6). codex_provider_event_projection_filter is a jq PROGRAM assembled by join — a foreign language built as a string, the same class the tmux pipe payload was in before it moved onto the bash backend (gunbc.dispatch_pipe_pane_emit). It is marked rather than fixed because the two available routes are not equivalent: minting a jq grammar to read backward would buy a modeled emitter for a filter that exists ONLY to bound a line before the substrate reads it, which is the purity trap DESIGN 6 names — the displaced cost is zero once the read itself is bounded. DISSOLVES ON: the observation reading the attempt JSONL directly under a substrate-side line bound instead of shelling to a projector — the witness-realization lane's typed file read (docs/plans/witness-realization-plan.md), at which point BOTH the filter string and the ProviderEventProjectionCapability jq dependency delete together. Until then the interpolated values are the two bounds above (both Int data rows), never caller input, and the classifier downstream is anchored and total, so a projector drift refuses rather than reclassifying." // Codex --json is the provider protocol authority: one JSON object per line with thread.started, -// turn.started, item.*, turn.completed, turn.budget_exhausted, turn.failed, and error event types. The raw stream is +// turn.started, item.*, turn.completed, turn.budget_exhausted, turn.failed, and error event types, +// plus the harness's own round.usage, turn.alignment_checkpoint_due and turn.wire_log_unwritable +// (gunbc.harness.harness_turn). The raw stream is // structurally reduced by codex_provider_event_projection_filter before this bounded total envelope // classifier runs. The shared RFC 8259 grammar proves each projected line parseable, then the // classifier requires the emitted top-level `type` member to be the first object member before @@ -369,6 +378,18 @@ fn codex_event_from_complete_line( raw: raw, kind: HarnessRoundUsage, } } + } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.alignment_checkpoint_due") { + Present { value: CodexProviderEvent { + line_number: line_number, + raw: raw, + kind: HarnessAlignmentCheckpointDue, + } } + } else if codex_line_has_top_level_type(raw: raw, event_type: "turn.wire_log_unwritable") { + Present { value: CodexProviderEvent { + line_number: line_number, + raw: raw, + kind: HarnessWireLogUnwritable, + } } } else if codex_line_has_top_level_type(raw: raw, event_type: "error") { Present { value: CodexProviderEvent { line_number: line_number, @@ -539,6 +560,12 @@ fn provider_execution_fold_event( CodexThreadStarted => ProviderStarting { activity: "thread started" } CodexTurnStarted => ProviderRunning { activity: "agent started" } HarnessRoundUsage => state + HarnessWireLogUnwritable => state + HarnessAlignmentCheckpointDue => + ProviderBudgetSpent { + activity: "Alignment checkpoint due · adjudication decides resume", + detail: event.raw, + } CodexItemStarted { activity } => ProviderRunning { activity: join( diff --git a/dag/gunbc/roadmap/roadmap_serve.dag b/dag/gunbc/roadmap/roadmap_serve.dag index b89d6f31cc8..ea783a53074 100644 --- a/dag/gunbc/roadmap/roadmap_serve.dag +++ b/dag/gunbc/roadmap/roadmap_serve.dag @@ -10,7 +10,7 @@ import extdeps.http.form_urlencoded { form_decode, form_field, FormDecoded, Form import gunbc.roadmap_allocation_location { allocation_page_path } import gunbc.auth.profile_projection { - profile_store_layout_for_instance, profile_store_upsert_from_session, profile_store_read_all, + profile_store_layout_for_instance, profile_store_upsert_from_session, profile_store_read_all, ProfileStoreReadAll, ProfileStoreUpserted, ProfileStoreUpsertRefused, ProfileStoreRead, ProfileStoreUnread, PrincipalProfile, } import gunbc.roadmap_allocation_page { allocation_page, allocation_client_path } @@ -156,8 +156,15 @@ import gunbc.roadmap_launch_admission { import gunbc.roadmap_page { roadmap_page_for_projection_launch, roadmap_fleet_page, issue_detail_page_for, issue_detail_node_exists, + project_suite_headline, project_suite_not_found_page, project_suite_page_launch, project_suite_rows, } -import gunbc.roadmap_authority { RoadmapAuthorityProjection, roadmap_authority_projection } +import gunbc.roadmap_authority { + RoadmapAuthorityProjection, roadmap_authority_projection, + RoadmapAuthorityProjected, RoadmapAuthorityProjectionRefused, + declared_roadmap_edges, +} +import gunbc.roadmap_nesting { declared_roadmap_memberships } +import gunbc.roadmap_spawner { doc_all_nodes } import gunbc.roadmap_component { dispatch_client_js_source, roadmap_dispatch_asset_path, sandbox_client_js_source, sandbox_asset_path, @@ -345,6 +352,7 @@ type RoadmapServeHandler | IssueAssignHandler | IssueAssignmentContextHandler | IssueCommentHandler + | ProjectSuitePageHandler | AuthLoginHandler | AuthGoogleCallbackHandler | AuthLogoutHandler @@ -437,6 +445,7 @@ fn roadmap_serve_route_specs() -> List { RoadmapServeRouteSpec { method: POST, raw: "/sandbox/echo", handler: SandboxEchoHandler }, RoadmapServeRouteSpec { method: POST, raw: "/dispatch/\{node_id\}", handler: BeltDispatchHandler }, RoadmapServeRouteSpec { method: GET, raw: "/issue/\{node_id\}", handler: IssueDetailPageHandler }, + RoadmapServeRouteSpec { method: GET, raw: "/project/\{node_id\}", handler: ProjectSuitePageHandler }, RoadmapServeRouteSpec { method: GET, raw: "/issue/\{node_id\}/assignment", handler: IssueAssignmentContextHandler }, RoadmapServeRouteSpec { method: POST, raw: "/issue/\{node_id\}/assign", handler: IssueAssignHandler }, RoadmapServeRouteSpec { method: POST, raw: "/issue/\{node_id\}/comment", handler: IssueCommentHandler }, @@ -896,12 +905,51 @@ fn serve_issue_detail_response_over( } } } +// THE PROJECT SUITE ROUTE (GET /project/): ONE project's direct membership listing +// rendered live from the same authorities the issue detail route reads -- the declared nesting +// for the member set, the declared dependency edges for the topological layering, the plan's own +// rows for the titles. The heading is the project row's own ticket headline; an absent row or a +// non-ticket row renders the not-found document at 404, the issue route's grain. The page carries +// no form and no session secret, so it renders for every viewer standing exactly alike; the +// viewer label is still resolved here, so the render entry keeps the issue route's signature +// shape. A refused plan is a located 503, never a silent empty page. +fn serve_project_suite_response_over( + plan: RoadmapAuthorityProjection, + params: List, + request: ServeHttpRequest, +) -> ServeHttpResponse { + let node_id = path_param_value(params: params, name: "node_id") + let viewer = authenticated_viewer_label(evidence: request.security.authentication) + match plan { + RoadmapAuthorityProjectionRefused { detail: detail } => + serve_text_response(status: 503, content_type: text_plain_utf8, + body: join(["project suite unavailable: the roadmap authority refused at ", detail], "")) + RoadmapAuthorityProjected { doc, accepted: _, withheld: _ } => + match project_suite_headline(nodes: doc_all_nodes(doc: doc), node_id: node_id) { + Absent => + serve_page_response_with_status(page: project_suite_not_found_page(node_id: node_id, viewer: viewer), status: 404) + Present { value: headline } => + serve_page_response_with_status( + page: project_suite_page_launch( + headline: headline, + node_id: node_id, + rows: project_suite_rows( + memberships: declared_roadmap_memberships(), + edges: declared_roadmap_edges(), + nodes: doc_all_nodes(doc: doc), + project: node_id), + viewer: viewer), + status: 200) + } + } +} + // THE AUTH PROFILE ROSTER, read once per render under the never-blocks law: the store's refusal // (or absence) degrades to the empty roster — the page renders every assignee surface's fallback // exactly as before, never a failed page on a profile read. fn serve_profiles_for_instance(instance: HostDashboardInstance) -> List { match profile_store_read_all(layout: profile_store_layout_for_instance(instance: instance)) { - ProfileStoreRead { profiles } => profiles + ProfileStoreRead { profiles, skipped: _ } => profiles ProfileStoreUnread { step: _, reason: _ } => [] } } @@ -1403,13 +1451,14 @@ fn roadmap_serve_invoke_over( ApprovalStatusHandler => approval_status_response(params: params) SandboxEchoHandler => sandbox_echo_response(request: request) IssueDetailPageHandler => serve_issue_detail_response_over(instance: instance, plan: plan, params: params, request: request) + ProjectSuitePageHandler => serve_project_suite_response_over(plan: plan, params: params, request: request) IssueAssignmentContextHandler => serve_issue_assignment_context_over(instance: instance, plan: plan, request: request, params: params) IssueAssignHandler => serve_issue_assign_response_over(instance: instance, plan: plan, request: request, params: params) IssueCommentHandler => serve_issue_comment_response_over(instance: instance, request: request, params: params) AuthLoginHandler => auth_login_response(instance: instance, request: request) AuthGoogleCallbackHandler => auth_google_callback_response(instance: instance, path: request.path) AuthLogoutHandler => auth_logout_response(instance: instance, request: request) - AuthSessionHandler => auth_session_response(request: request) + AuthSessionHandler => auth_session_response_over(instance: instance, request: request) SessionsJsonHandler => serve_sessions_response_for_instance(instance: instance) AttemptsJsonHandler => serve_attempts_response_for_instance(instance: instance) WorkflowJsonHandler => serve_workflow_response_for_instance(instance: instance) @@ -2797,6 +2846,89 @@ fn auth_session_response(request: ServeHttpRequest) -> ServeHttpResponse { + let roster = profile_store_read_all(layout: profile_store_layout_for_instance(instance: instance)) + serve_text_response( + status: 200, + content_type: application_json_utf8, + body: auth_session_body_over(roster: roster, evidence: request.security.authentication), + ) +} + +fn auth_session_roster_standing_members(roster: ProfileStoreReadAll) -> List { + let unreadable = [ json_kv(key: "profile_standing", value: json_string(s: "profile store unread; session profile only")) ] as List + let partial = [ json_kv(key: "profile_standing", value: json_string(s: "one or more profile records unreadable; the affected members render their fallback")) ] as List + match roster { + ProfileStoreUnread { step: _, reason: _ } => unreadable + ProfileStoreRead { profiles: _, skipped } => + if count(skipped) == 0 { + [] as List + } else { + partial + } + } +} + +fn auth_session_body_over(roster: ProfileStoreReadAll, evidence: RequestAuthenticationEvidence) -> String { + serialize_json(v: json_object(members: match evidence { + GoogleOidcSession { session } => { + let found = match roster { + ProfileStoreRead { profiles, skipped: _ } => + filter(profiles, p => principal_ref_eq(a: p.principal, b: session.principal)).first() + ProfileStoreUnread { step: _, reason: _ } => none + } + let name = match found { + Present { value: stored } => match stored.name { + Present { value: n } => Present { value: n } + Absent => session.profile.name + } + Absent => session.profile.name + } + let email = match found { + Present { value: stored } => match stored.email { + Present { value: e } => Present { value: e } + Absent => session.profile.email + } + Absent => session.profile.email + } + let picture = match found { + Present { value: stored } => match stored.picture { + Present { value: u } => Present { value: u } + Absent => session.profile.picture + } + Absent => session.profile.picture + } + concat([ + json_kv(key: "authenticated", value: json_bool(b: true)), + json_kv(key: "principal", value: json_string(s: principal_ref_label(p: session.principal))), + json_kv(key: "expires_at", value: json_string(s: session.expires_at)), + json_kv(key: "viewer", value: json_string(s: authenticated_viewer_label(evidence: evidence))), + ], concat(auth_session_picture_members(picture: picture), concat(auth_session_name_members(name: name), concat(auth_session_email_members(email: email), auth_session_roster_standing_members(roster: roster))))) + } + SessionStandingInvalid { standing } => [ + json_kv(key: "authenticated", value: json_bool(b: false)), + json_kv(key: "standing", value: json_string(s: session_cookie_standing_reason(standing: standing))), + json_kv(key: "viewer", value: json_string(s: "")), + ] + FabricServiceEvidence { kind: _, authenticated_at: _ } => [ + json_kv(key: "authenticated", value: json_bool(b: false)), + json_kv(key: "standing", value: json_string(s: "service evidence is not a human session")), + json_kv(key: "viewer", value: json_string(s: authenticated_viewer_label(evidence: evidence))), + ] + AuthAbsent => [ + json_kv(key: "authenticated", value: json_bool(b: false)), + json_kv(key: "standing", value: json_string(s: session_cookie_standing_reason(standing: SessionCookieAbsent))), + json_kv(key: "viewer", value: json_string(s: "")), + ] + })) +} + // THE OPTIONAL PICTURE MEMBER of the session wire: present only when the session's profile // projected one — absence is the member's ABSENCE, never an empty string standing in for none, // and the client reads absence (like a failed fetch) as the initials fallback, never an diff --git a/dag/gunbc/roadmap/roadmap_served_observation.dag b/dag/gunbc/roadmap/roadmap_served_observation.dag index ce3f61ca881..ec75e6d1f83 100644 --- a/dag/gunbc/roadmap/roadmap_served_observation.dag +++ b/dag/gunbc/roadmap/roadmap_served_observation.dag @@ -254,7 +254,7 @@ fn served_observation_daily_surface_over(instance: HostDashboardInstance, plan: fn served_observation_profiles_for_instance(instance: HostDashboardInstance) -> List { match profile_store_read_all(layout: profile_store_layout_for_instance(instance: instance)) { - ProfileStoreRead { profiles } => profiles + ProfileStoreRead { profiles, skipped: _ } => profiles ProfileStoreUnread { step: _, reason: _ } => [] } } diff --git a/dag/test/claim/harness_alignment_checkpoint_test.dag b/dag/test/claim/harness_alignment_checkpoint_test.dag new file mode 100644 index 00000000000..daca22d30c3 --- /dev/null +++ b/dag/test/claim/harness_alignment_checkpoint_test.dag @@ -0,0 +1,46 @@ +module test.claim.harness_alignment_checkpoint_test + +import std.types { Bool, String } +import std.process { ExitSuccess, ExitFailure } +import gunbc.harness.harness_turn { + HarnessTurnOutcome, TurnAlignmentCheckpointDue, TurnStepBudgetExhausted, + harness_outcome_label, harness_outcome_detail, harness_event_alignment_checkpoint_due, + harness_budget_notice_text, +} +import gunbc.harness.harness_cli { harness_turn_exit } + +// THE EPOCH BOUNDARY IS AN ANNOUNCEMENT, NOT AN ADJUDICATION. The harness emits one durable event +// naming the chain fingerprint the attempt spawned with, and the outcome is typed -- no provider +// call, no worker self-report, no verdict minted inside the worker loop (review 5354823380). +test fn the_checkpoint_due_event_binds_the_chain_fingerprint() -> Bool { + let event = harness_event_alignment_checkpoint_due(steps_taken: 40, chain_fingerprint: "fnv1a64:abc123") + event.contains("turn.alignment_checkpoint_due") + && event.contains("fnv1a64:abc123") + && event.contains("40") +} + +// CHECKPOINT DUE IS ITS OWN OUTCOME, spelled so no consumer can read it as failure, completion, or +// a spent budget -- and its exit standing is the distinct admitted code 3, a planned control +// transfer, never the generic failure code 1 (review 5358022153). +test fn checkpoint_due_is_neither_failure_nor_budget_exhaustion() -> Bool { + let due = TurnAlignmentCheckpointDue { steps_taken: 40 } + let spent = TurnStepBudgetExhausted { steps_taken: 40 } + harness_outcome_label(outcome: due) == "alignment-checkpoint-due" + && harness_outcome_label(outcome: spent) == "step-budget-exhausted" + && harness_outcome_detail(outcome: due).contains("not a failure") + && harness_outcome_detail(outcome: due).contains("adjudication") + && (match harness_turn_exit(outcome: due) { ExitSuccess => false ExitFailure { code, reason: _ } => code == 3 }) +} + +// THE ROUND NOTICE SAYS WHAT ZERO MEANS. A chainless turn ends at zero; a chain-carrying turn +// returns control for independent adjudication there (review 5358022153) -- the notice must +// neither threaten a finish line that does not exist nor claim the harness itself runs a check. +test fn the_notice_names_the_checkpoint_when_the_turn_carries_a_chain() -> Bool { + let chained = harness_budget_notice_text(steps_remaining: 3, checkpointed: true) + let chainless = harness_budget_notice_text(steps_remaining: 3, checkpointed: false) + chained.contains("independent alignment checkpoint") + && chained.contains("not task completion or task failure") + && chained.contains("may resume") + && !chained.contains("make the change now") + && chainless.contains("make the change now") +} diff --git a/dag/test/claim/harness_chat_shape_test.dag b/dag/test/claim/harness_chat_shape_test.dag index 150b1f2c30e..da096f92b42 100644 --- a/dag/test/claim/harness_chat_shape_test.dag +++ b/dag/test/claim/harness_chat_shape_test.dag @@ -92,6 +92,7 @@ test fn chat_tool_results_are_tool_messages_then_the_notice() -> Bool { shape: OpenAiChatCompletionsShape, results: [HarnessToolResult { tool_use_id: "chatcmpl-tool-1", text: "a b c\n[exit 0]", is_error: false }], steps_remaining: 7, + checkpointed: false, ) list_length(items: msgs) == 2 && (match msgs.first() { Present { value: m } => serialize_json(v: m).contains("\"role\": \"tool\"") && serialize_json(v: m).contains("chatcmpl-tool-1") Absent => false }) @@ -103,6 +104,7 @@ test fn the_messages_shape_still_sends_one_user_message_of_results() -> Bool { shape: AnthropicMessagesShape, results: [HarnessToolResult { tool_use_id: "t1", text: "x", is_error: false }], steps_remaining: 7, + checkpointed: false, )) == 1 } diff --git a/dag/test/claim/harness_conversation_fork_test.dag b/dag/test/claim/harness_conversation_fork_test.dag new file mode 100644 index 00000000000..ae782659b6b --- /dev/null +++ b/dag/test/claim/harness_conversation_fork_test.dag @@ -0,0 +1,170 @@ +module test.claim.harness_conversation_fork_test + +import std.types { Bool, String } +import gunbc.harness.harness_conversation_fork { + ConversationForkPoint, ConversationForkAdmittance, ForkAdmitted, ForkInvalid, conversation_fork_admit, + KvPrefixResidency, Resident, NotResident, Unobserved, Evicted, Incompatible, + KvPrefixResidencyObservation, KvPlacementPreference, PreferWarmLocation, ExecuteColdFromDurableState, + kv_prefix_residency_placement, +} + +// WITNESSES FOR THE PURE HALF of the conversation-fork model (review 5358022153, lane D): the +// fork admittance law and the KV-residency observation. The harness IO half -- the durable wire +// log itself -- is deliberately not wet-tested here; these claims are about the typed decisions, +// not the filesystem. + +fn fork_point_fixture() -> ConversationForkPoint { + ConversationForkPoint { + attempt_lineage: "lineage-abc", + epoch: 2, + round: 7, + request_digest: "fnv1a64:req7", + response_digest: "fnv1a64:resp7", + model_identity: "deepseek-v4@serving-a", + wire_shape: "anthropic-messages", + } +} + +// A FORK IS ADMITTED WHEN EVERY IDENTITY HOLDS, and the admission carries the point itself -- the +// lineage, epoch, round and both wire digests all survive the admit, so a consumer of the +// admittance has the fork address without re-deriving it. +test fn a_fork_point_with_exact_identity_match_is_admitted() -> Bool { + match conversation_fork_admit( + point: fork_point_fixture(), + model_identity: "deepseek-v4@serving-a", + wire_shape: "anthropic-messages", + chain_fingerprint_at_point: "fnv1a64:chain1", + chain_fingerprint_now: "fnv1a64:chain1", + ) { + ForkAdmitted { point } => + point.attempt_lineage == "lineage-abc" + && point.epoch == 2 + && point.round == 7 + && point.request_digest == "fnv1a64:req7" + && point.response_digest == "fnv1a64:resp7" + ForkInvalid { cause: _ } => false + } +} + +// EACH INVALIDATION REFUSES WITH ITS OWN SENTENCE, naming both the identity that moved and its two +// spellings. The model-change cause is the law's exact words -- a fork across a model change +// replays context the engine never computed. +test fn a_fork_across_a_model_change_is_invalid_with_its_named_cause() -> Bool { + match conversation_fork_admit( + point: fork_point_fixture(), + model_identity: "glm-5.3@serving-b", + wire_shape: "anthropic-messages", + chain_fingerprint_at_point: "fnv1a64:chain1", + chain_fingerprint_now: "fnv1a64:chain1", + ) { + ForkAdmitted { point: _ } => false + ForkInvalid { cause } => + string_contains(s: cause, pattern: "a fork across a model change replays context the engine never computed") + && string_contains(s: cause, pattern: "deepseek-v4@serving-a") + && string_contains(s: cause, pattern: "glm-5.3@serving-b") + } +} + +test fn a_fork_across_a_wire_shape_change_is_invalid_with_its_named_cause() -> Bool { + match conversation_fork_admit( + point: fork_point_fixture(), + model_identity: "deepseek-v4@serving-a", + wire_shape: "openai-chat-completions", + chain_fingerprint_at_point: "fnv1a64:chain1", + chain_fingerprint_now: "fnv1a64:chain1", + ) { + ForkAdmitted { point: _ } => false + ForkInvalid { cause } => + string_contains(s: cause, pattern: "wire-shape change") + && string_contains(s: cause, pattern: "anthropic-messages") + && string_contains(s: cause, pattern: "openai-chat-completions") + } +} + +test fn a_fork_after_the_chain_moved_is_invalid_with_its_named_cause() -> Bool { + match conversation_fork_admit( + point: fork_point_fixture(), + model_identity: "deepseek-v4@serving-a", + wire_shape: "anthropic-messages", + chain_fingerprint_at_point: "fnv1a64:chain1", + chain_fingerprint_now: "fnv1a64:chain2", + ) { + ForkAdmitted { point: _ } => false + ForkInvalid { cause } => + string_contains(s: cause, pattern: "the alignment chain the context was built against changed") + && string_contains(s: cause, pattern: "fnv1a64:chain1") + && string_contains(s: cause, pattern: "fnv1a64:chain2") + } +} + +fn residency_label(standing: KvPrefixResidency) -> String { + match standing { + Resident => "resident" + NotResident => "not-resident" + Unobserved => "unobserved" + Evicted => "evicted" + Incompatible => "incompatible" + } +} + +// THE FIVE STANDINGS ARE DISTINCT VARIANTS, not degrees of one Bool: every pairing answers +// differently, so a consumer cannot smuggle "evicted means not resident means unknown" through a +// shared label. +test fn residency_standings_are_five_distinct_variants() -> Bool { + let a = residency_label(standing: Resident) + let b = residency_label(standing: NotResident) + let c = residency_label(standing: Unobserved) + let d = residency_label(standing: Evicted) + let e = residency_label(standing: Incompatible) + a != b && a != c && a != d && a != e + && b != c && b != d && b != e + && c != d && c != e + && d != e +} + +fn prefers_warm(preference: KvPlacementPreference) -> Bool { + match preference { + PreferWarmLocation => true + ExecuteColdFromDurableState => false + } +} + +// PLACEMENT FOLLOWS OBSERVATION, AND THE DEFAULT IS HONEST: only an observed Resident prefix +// earns the warm preference; Unobserved -- the question never asked -- lands on cold exactly like +// NotResident, Evicted and Incompatible. Cold is always available because the durable wire log, +// not any cache, is the correctness authority. +test fn only_resident_prefers_warm_and_unobserved_defaults_to_cold() -> Bool { + prefers_warm(kv_prefix_residency_placement(standing: Resident)) + && !prefers_warm(kv_prefix_residency_placement(standing: NotResident)) + && !prefers_warm(kv_prefix_residency_placement(standing: Unobserved)) + && !prefers_warm(kv_prefix_residency_placement(standing: Evicted)) + && !prefers_warm(kv_prefix_residency_placement(standing: Incompatible)) +} + +// THE OBSERVATION CARRIES EVERY IDENTITY A WARM LOCATION MUST MATCH -- model, checkpoint's +// serving unit, tokenizer, chat template, and both prompt digests -- because two serving units +// only share a KV prefix when all of them do; a shared chain fingerprint alone never establishes +// the same KV prefix. +test fn a_residency_observation_binds_the_identities_a_warm_location_must_match() -> Bool { + let observed = KvPrefixResidencyObservation { + serving_unit: "serving-a", + model_identity: "deepseek-v4", + tokenizer_identity: "tokenizer-v4", + chat_template_identity: "template-v4", + semantic_prompt_digest: "fnv1a64:sem", + wire_digest: "fnv1a64:wire", + standing: Unobserved, + observed_at: "2026-09-30T00:00:00Z", + } + match observed.standing { + Unobserved => + observed.serving_unit == "serving-a" + && observed.model_identity == "deepseek-v4" + && observed.tokenizer_identity == "tokenizer-v4" + && observed.chat_template_identity == "template-v4" + && observed.semantic_prompt_digest == "fnv1a64:sem" + && observed.wire_digest == "fnv1a64:wire" + && !prefers_warm(kv_prefix_residency_placement(standing: observed.standing)) + _ => false + } +} diff --git a/dag/test/claim/harness_guidance_test.dag b/dag/test/claim/harness_guidance_test.dag index 19ccc271c93..ba83d1ab1be 100644 --- a/dag/test/claim/harness_guidance_test.dag +++ b/dag/test/claim/harness_guidance_test.dag @@ -9,8 +9,11 @@ import gunbc.harness.harness_guidance { harness_worker_guidance_at, WorktreeAt, RoundsRemaining, ChangeTheTreeWithTheTool, VerifyWithTheTool, ReadingAloneCompletesNothing, grounded, harness_conclusions, harness_guidance_prose, + harness_guidance_segments, harness_guidance_prose_from_segments, harness_conclusion_nodes, + HarnessConclusionNode, harness_conclusion_key, harness_worker_guidance, harness_probe_guidance, } +import gunbc.harness.harness_prompt_context { AuthorityInput, DerivedConclusionInput, prompt_segment_required } import gunbc.harness.harness_tool { RunCommandTool, harness_tool_wire_name } import gunbc.roadmap.roadmap_submission { submission_schema, submission_decode, SubmissionDecoded, SubmissionNotDecodable, SubmissionReady, @@ -408,3 +411,62 @@ test fn the_production_shape_without_the_schema_member_is_not_a_declaration() -> SubmissionDecoded { submission: _ } => false } } + +// THE GUIDANCE POPULATION IS THE SEGMENT POPULATION: the prose projection and the modeled segments +// cannot fork, every premise segment names its grounding declaration at the caller's revision, and +// every conclusion segment names its grounds so a retracted premise moves the digest. +test fn the_segments_and_the_prose_are_one_population() -> Bool { + let rows = harness_worker_guidance_at( + worktree: "/wt", + test_command: "test-cmd", + test_receipt_path: "/wt/.receipts/test", + compile_command: "compile-cmd", + compile_receipt_path: "/wt/.receipts/compile", + ) + let segments = harness_guidance_segments(rows: rows, source_revision: "rev-fixture") + harness_guidance_prose_from_segments(segments: segments) == harness_guidance_prose(rows: rows) + && count(segments) > count(rows) + && (match segments.first() { + Present { value: s } => + (match s.source { + AuthorityInput { declaration: _, revision, required } => revision == "rev-fixture" && required + _ => false + }) + && prompt_segment_required(segment: s) + && s.content_digest != "" + Absent => false + }) + && (match segments.last() { + Present { value: s } => + match s.source { + DerivedConclusionInput { conclusion_key, grounds, required } => + conclusion_key != "" && required && count(grounds) > 0 && count(grounds) <= count(rows) + _ => false + } + Absent => false + }) +} + +test fn a_moved_revision_moves_the_segment_inputs() -> Bool { + let rows = harness_probe_guidance() + let a = harness_guidance_segments(rows: rows, source_revision: "rev-a") + let b = harness_guidance_segments(rows: rows, source_revision: "rev-b") + match a.first() { + Absent => false + Present { value: sa } => + match b.first() { + Absent => false + Present { value: sb } => + sa.content == sb.content + && sa.content_digest == sb.content_digest + && (match sa.source { + AuthorityInput { declaration: _, revision, required: _ } => revision == "rev-a" + _ => false + }) + && (match sb.source { + AuthorityInput { declaration: _, revision, required: _ } => revision == "rev-b" + _ => false + }) + } + } +} diff --git a/dag/test/claim/harness_prompt_context_test.dag b/dag/test/claim/harness_prompt_context_test.dag new file mode 100644 index 00000000000..7dd3fb310c2 --- /dev/null +++ b/dag/test/claim/harness_prompt_context_test.dag @@ -0,0 +1,182 @@ +module test.claim.harness_prompt_context_test + +import std.types { Bool, String, List } +import std.decl_ref { decl_ref } +import gunbc.harness.harness_prompt_context { + PromptInput, AuthorityInput, EvidenceInput, GuidanceInput, + ContextStanding, EstablishedObservation, Hypothesis, Withdrawn, + PromptContextSnapshot, prompt_snapshot_digest, prompt_input_canonical_text, + PromptSegment, prompt_segment_render, prompt_segment_required, + PromptPlan, PromptPlanAssembly, PromptPlanReady, PromptPlanRefused, + PromptAssemblyRefusal, RequiredSourceUnrendered, MissingRendering, DuplicateRendering, + UnexpectedRendering, WithdrawnSourceRendered, prompt_assembly_refusal_text, + prompt_plan_assemble, semantic_prompt_render, provider_prompt_artifact, prompt_render_receipt, +} + +fn fx_authority(id: String, revision: String) -> PromptInput { + AuthorityInput { + declaration: decl_ref(module_path: "gunbc.design_document", decl_name: "design_document"), + revision: join([id, revision], ""), + required: true, + } +} + +fn fx_evidence(standing: ContextStanding, required: Bool) -> PromptInput { + EvidenceInput { artifact: "receipt-x", subject: "claim-y", standing: standing, required: required } +} + +fn fx_snapshot(inputs: List) -> PromptContextSnapshot { + PromptContextSnapshot { + attempt_lineage: "worker:task-a-deadbeef", + epoch: 1, + source_revision: "rev-src", + work_contract_revision: "rev-contract", + inputs: inputs, + } +} + +fn fx_seg(input: PromptInput, content: String) -> PromptSegment { + prompt_segment_render(purpose: "test segment", source: input, content: content) +} + +// THE SNAPSHOT DIGEST IS DERIVATION-EXACT: identical inputs digest identically, and any moved +// revision moves it. +test fn identical_snapshots_digest_identically_and_any_revision_moves_it() -> Bool { + let base = prompt_snapshot_digest(snapshot: fx_snapshot(inputs: [fx_authority(id: "a", revision: "r1")])) + base == prompt_snapshot_digest(snapshot: fx_snapshot(inputs: [fx_authority(id: "a", revision: "r1")])) + && base != prompt_snapshot_digest(snapshot: fx_snapshot(inputs: [fx_authority(id: "a", revision: "r2")])) + && base != "" +} + +// STANDING IS IDENTITY (review 5358022153): the same artifact as a hypothesis and as an +// established observation are two different inputs, and a length-prefixed preimage cannot be +// manufactured by a field containing a separator. +test fn a_standing_change_is_a_different_input_not_a_restated_one() -> Bool { + prompt_input_canonical_text(input: fx_evidence(standing: Hypothesis, required: false)) + != prompt_input_canonical_text(input: fx_evidence(standing: EstablishedObservation, required: false)) + && prompt_input_canonical_text(input: fx_evidence(standing: EstablishedObservation, required: false)) + != prompt_input_canonical_text(input: fx_evidence(standing: EstablishedObservation, required: true)) +} + +// ASSEMBLY IS THE EXACT JOIN: a withdrawn input renders ZERO segments; a required input that never +// rendered refuses; an input rendered twice refuses; a segment foreign to the snapshot refuses. +test fn the_assembly_join_is_exact() -> Bool { + let authority = fx_authority(id: "a", revision: "r1") + let withdrawn = fx_evidence(standing: Withdrawn, required: false) + let snapshot = fx_snapshot(inputs: [authority, withdrawn]) + let ready = prompt_plan_assemble( + snapshot: snapshot, + rendered: [fx_seg(input: authority, content: "the principles pointer")], + ) + let unrendered = prompt_plan_assemble(snapshot: fx_snapshot(inputs: [authority]), rendered: []) + let duplicated = prompt_plan_assemble( + snapshot: fx_snapshot(inputs: [authority]), + rendered: [fx_seg(input: authority, content: "one"), fx_seg(input: authority, content: "two")], + ) + let foreign = prompt_plan_assemble( + snapshot: fx_snapshot(inputs: [authority]), + rendered: [ + fx_seg(input: authority, content: "the principles pointer"), + fx_seg(input: fx_authority(id: "b", revision: "r1"), content: "foreign segment"), + ], + ) + let withdrawn_rendered = prompt_plan_assemble( + snapshot: snapshot, + rendered: [ + fx_seg(input: authority, content: "the principles pointer"), + fx_seg(input: withdrawn, content: "stale claim"), + ], + ) + (match ready { + PromptPlanReady { plan } => + count(plan.ordered_segments) == 1 + && (match plan.ordered_segments.first() { + Present { value: s } => s.content == "the principles pointer" && prompt_segment_required(segment: s) + Absent => false + }) + PromptPlanRefused { refusals: _ } => false + }) + && (match unrendered { + PromptPlanRefused { refusals } => + count(refusals) == 1 && (match refusals.first() { + Present { value: RequiredSourceUnrendered { input_key: _ } } => true + _ => false + }) + PromptPlanReady { plan: _ } => false + }) + && (match duplicated { + PromptPlanRefused { refusals } => + match refusals.first() { + Present { value: DuplicateRendering { input_key: _ } } => true + _ => false + } + PromptPlanReady { plan: _ } => false + }) + && (match foreign { + PromptPlanRefused { refusals } => + any(refusals, r => match r { + UnexpectedRendering { source_key: _ } => true + _ => false + }) + PromptPlanReady { plan: _ } => false + }) + && (match withdrawn_rendered { + PromptPlanRefused { refusals } => + any(refusals, r => match r { + WithdrawnSourceRendered { input_key: _ } => true + _ => false + }) + PromptPlanReady { plan: _ } => false + }) +} + +// THE SEGMENT ORDER IS THE SNAPSHOT'S, not the caller's render order. +test fn the_plan_orders_segments_by_snapshot_input_order() -> Bool { + let a = fx_authority(id: "a", revision: "r1") + let b = GuidanceInput { receipt_identity: "g1", resolved: false, required: true } + match prompt_plan_assemble( + snapshot: fx_snapshot(inputs: [a, b]), + rendered: [fx_seg(input: b, content: "guidance first rendered"), fx_seg(input: a, content: "authority")], + ) { + PromptPlanRefused { refusals: _ } => false + PromptPlanReady { plan } => + match plan.ordered_segments.first() { + Present { value: s } => s.content == "authority" + Absent => false + } + } +} + +// THE SEMANTIC ARTIFACT carries exact extents over the joined text, and the provider artifacts of +// one plan share the semantic digest while their wire digests differ. +test fn the_semantic_and_wire_identities_are_distinct() -> Bool { + let a = fx_authority(id: "a", revision: "r1") + let b = GuidanceInput { receipt_identity: "g1", resolved: false, required: true } + match prompt_plan_assemble( + snapshot: fx_snapshot(inputs: [a, b]), + rendered: [fx_seg(input: a, content: "alpha"), fx_seg(input: b, content: "beta")], + ) { + PromptPlanRefused { refusals: _ } => false + PromptPlanReady { plan } => { + let semantic = semantic_prompt_render(plan: plan) + let anthropic = provider_prompt_artifact(semantic: semantic, provider_projection: "anthropic", wire_text: "{\"system\": \"...\"}") + let openai = provider_prompt_artifact(semantic: semantic, provider_projection: "openai-chat", wire_text: "{\"messages\": [...]}") + let receipt_a = prompt_render_receipt(plan: plan, wire: anthropic) + semantic.text == "alpha\nbeta" + && count(semantic.extents) == 2 + && (match semantic.extents.first() { + Present { value: e } => e.start == 0 && e.end == 5 + Absent => false + }) + && (match semantic.extents.last() { + Present { value: e } => e.start == 6 && e.end == 10 + Absent => false + }) + && anthropic.semantic_digest == openai.semantic_digest + && anthropic.wire_digest != openai.wire_digest + && receipt_a.semantic_digest == semantic.semantic_digest + && receipt_a.plan_snapshot_digest == plan.snapshot_digest + && count(receipt_a.input_digests) == 2 + } + } +} diff --git a/dag/test/claim/long/roadmap_page_witness_test.dag b/dag/test/claim/long/roadmap_page_witness_test.dag index c9686b738ad..bc7ed8fdfb5 100644 --- a/dag/test/claim/long/roadmap_page_witness_test.dag +++ b/dag/test/claim/long/roadmap_page_witness_test.dag @@ -1595,7 +1595,7 @@ fn issue_detail_fixture_panel() -> gunbc.roadmap_presentation.IssueLinkPanel { rn: gunbc.roadmap_authority.authored(identity: "rnfx_idspec", id: "issue-detail-specimen", done: false, content: "Specimen issue"), nodes: nodes, 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" }]) + memberships: [gunbc.roadmap_nesting.RoadmapMembership { member: "issue-detail-specimen", project: "issue-detail-parent", contribution: "fixture contribution" }, gunbc.roadmap_nesting.RoadmapMembership { member: "issue-detail-child", project: "issue-detail-specimen", contribution: "fixture contribution" }]) } fn fixture_human_ref() -> gunbc.principal_projection.PrincipalRef { @@ -2585,7 +2585,7 @@ test fn witness_inline_assignment_context_chip_html_carries_the_profile_display( // These older fixtures explicitly declare both relations with the same endpoints. Production // never derives ownership this way; the independent-relation control uses different endpoints. fn fixture_nesting_relation(edges: List) -> List { - map(edges, e => gunbc.roadmap_nesting.RoadmapMembership { member: e.parent as String, project: e.child as String }) + map(edges, e => gunbc.roadmap_nesting.RoadmapMembership { member: e.parent as String, project: e.child as String, contribution: "fixture contribution" }) } test fn renamed_field_label_keeps_its_typed_section_and_brief_renders_once() -> Bool { diff --git a/dag/test/claim/roadmap/roadmap_alignment_witness_test.dag b/dag/test/claim/roadmap/roadmap_alignment_witness_test.dag index f711d5e4e8a..a3fc0117cf4 100644 --- a/dag/test/claim/roadmap/roadmap_alignment_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_alignment_witness_test.dag @@ -14,14 +14,14 @@ import std.decl_ref { DeclarationRef, decl_ref, decl_field_eq, WholeDeclaration import gunbc.roadmap.roadmap_alignment { AlignmentChain, AlignmentChainCyclic, AlignmentChainLink, - AlignmentChained, AlignmentChainAmbiguous, AlignmentChainRowUndeclared, + AlignmentChained, AlignmentChainAmbiguous, AlignmentChainUnrooted, AlignmentChainRowUndeclared, alignment_chain, alignment_chain_for, AlignmentLevel, TaskContractLevel, ProjectIntentLevel, RootPrinciplesLevel, AlignmentResolvedChain, alignment_levels, alignment_levels_due, AlignmentRefusal, - AlignmentChainForked, AlignmentRowUndeclared, AlignmentNestingCyclic, + AlignmentChainForked, AlignmentWalkUnrooted, AlignmentEdgeContributionRefused, AlignmentRowUndeclared, AlignmentNestingCyclic, alignment_refusal_reason, AlignmentResolution, AlignmentReady, AlignmentUnresolved, @@ -49,7 +49,7 @@ import gunbc.roadmap.roadmap_attempt_continuation { } import gunbc.roadmap_model { RoadmapNode, RoadmapNodeId, RoadmapNodeIdentity, Pointer, Unsized } import gunbc.roadmap_status { TicketLine, TicketFields, Unsigned } -import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } +import gunbc.roadmap_execution_contract { ExecutionContract, ExecutionContractUnspecified, NotAgentWork } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -89,7 +89,7 @@ fn fixture_node(id: String, headline: String, brief: String) -> RoadmapNode { } fn fixture_edge(child: String, parent: String) -> RoadmapMembership { - RoadmapMembership { project: child, member: parent } + RoadmapMembership { project: child, member: parent, contribution: join(["fixture contribution of ", parent, " to ", child], "") } } fn fixture_nodes() -> List { @@ -100,6 +100,7 @@ fn fixture_nodes() -> List { fixture_node(id: "arc-two", headline: "Arc Two headline", brief: "Arc Two boundary"), fixture_node(id: "arc-three", headline: "Arc Three headline", brief: "Arc Three boundary"), fixture_node(id: "lone-task", headline: "Lone headline", brief: "Lone boundary"), + fixture_node(id: "gunbc-project-root", headline: "Root project headline", brief: "Root project boundary"), ] } @@ -107,6 +108,7 @@ fn linear_edges() -> List { [ fixture_edge(child: "arc-one", parent: "task-a"), fixture_edge(child: "arc-two", parent: "arc-one"), + fixture_edge(child: "gunbc-project-root", parent: "arc-two"), ] } @@ -115,6 +117,7 @@ fn deep_linear_edges() -> List { fixture_edge(child: "arc-one", parent: "task-a"), fixture_edge(child: "arc-mid", parent: "arc-one"), fixture_edge(child: "arc-two", parent: "arc-mid"), + fixture_edge(child: "gunbc-project-root", parent: "arc-two"), ] } @@ -139,9 +142,11 @@ test fn a_linear_chain_runs_task_then_projects_then_the_root_pointer() -> Bool { task.node_id == "task-a" && task.purpose == "Task A headline" && task.boundary == "Task A boundary" - && (map(projects, p => p.node_id)) == ["arc-one", "arc-two"] - && (map(projects, p => p.purpose)) == ["Arc One headline", "Arc Two headline"] - && (map(projects, p => p.boundary)) == ["Arc One boundary", "Arc Two boundary"] + && (map(projects, p => p.node_id)) == ["arc-one", "arc-two", "gunbc-project-root"] + && (map(projects, p => p.purpose)) == ["Arc One headline", "Arc Two headline", "Root project headline"] + && (map(projects, p => p.boundary)) == ["Arc One boundary", "Arc Two boundary", "Root project boundary"] + && task.contribution_to_parent == "fixture contribution of task-a to arc-one" + && (map(projects, p => p.contribution_to_parent)) == ["fixture contribution of arc-one to arc-two", "fixture contribution of arc-two to gunbc-project-root", ""] && root.module_path as String == "gunbc.design_document" && root.decl_name as String == "design_document" && decl_field_eq(a: root.field, b: WholeDeclaration) @@ -155,15 +160,27 @@ test fn an_edge_edit_re_renders_the_chain_there_is_no_second_list() -> Bool { match alignment_chain(node_id: "task-a", memberships: deep_linear_edges(), nodes: fixture_nodes()) { AlignmentChained { task, projects, root: _ } => task.node_id == "task-a" - && (map(projects, p => p.node_id)) == ["arc-one", "arc-mid", "arc-two"] + && (map(projects, p => p.node_id)) == ["arc-one", "arc-mid", "arc-two", "gunbc-project-root"] _ => false } } -test fn a_projectless_node_chains_straight_from_its_task_to_the_root() -> Bool { +// A STANDALONE END THAT IS NOT THE ADMITTED ROOT REFUSES (review 5354823380): lone-task has no +// membership arc and is not a declared root, so the walk never reached the top of a functional +// decomposition and the chain says so with the walked prefix intact. The admitted root itself is +// the one node whose parentless chain resolves. +test fn a_projectless_node_that_is_not_the_admitted_root_refuses_unrooted() -> Bool { match alignment_chain(node_id: "lone-task", memberships: [], nodes: fixture_nodes()) { + AlignmentChainUnrooted { walked, at } => at == "lone-task" && count(walked) == 0 + _ => false + } +} + +test fn the_admitted_roots_own_chain_resolves_with_no_parent_contribution() -> Bool { + match alignment_chain(node_id: "gunbc-project-root", memberships: [], nodes: fixture_nodes()) { AlignmentChained { task, projects, root } => - task.node_id == "lone-task" + task.node_id == "gunbc-project-root" + && task.contribution_to_parent == "" && count(projects) == 0 && root.module_path as String == "gunbc.design_document" _ => false @@ -221,7 +238,7 @@ test fn an_undeclared_arc_row_mid_walk_refuses_rather_than_projecting_nothing() test fn production_blockers_do_not_invent_project_ownership() -> Bool { match alignment_chain_for(node_id: "shell-dag-cron-entry-line-builder") { - AlignmentChained { task, projects, root: _ } => task.node_id == "shell-dag-cron-entry-line-builder" && map(projects, p => p.node_id) == ["shell-typed-invocation"] + AlignmentChained { task, projects, root: _ } => task.node_id == "shell-dag-cron-entry-line-builder" && map(projects, p => p.node_id) == ["shell-typed-invocation", "gunbc-project-root"] _ => false } } @@ -477,11 +494,11 @@ test fn an_unknown_verdict_kind_refuses() -> Bool { // claims pass the variant to the resolution fold, never the record. fn four_level_resolved() -> AlignmentResolvedChain { AlignmentResolvedChain { - task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary" }, + task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, projects: [ - AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary" }, - AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary" }, - AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary" }, + AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, ], root: alignment_root_pointer_fixture(), } @@ -489,11 +506,11 @@ fn four_level_resolved() -> AlignmentResolvedChain { fn four_level_chained() -> AlignmentChain { AlignmentChained { - task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary" }, + task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, projects: [ - AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary" }, - AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary" }, - AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary" }, + AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, ], root: alignment_root_pointer_fixture(), } @@ -524,13 +541,21 @@ test fn turn_zero_is_dispatch_not_a_checkpoint_nothing_is_due() -> Bool { && count(alignment_levels_due(chain: four_level_resolved(), turns_elapsed: 0 - 5, state: base_state())) == 0 } -test fn the_task_level_is_due_first_in_every_due_set() -> Bool { +// DUE SETS READ ROOT TO LEAF like the levels themselves: at turn 2000 the arc-mid project (cadence +// index 2, interval 2000), the arc-one project (index 1, interval 200) and the task (index 0, +// interval 20) are due, and the task -- the most frequently checked level -- closes the list. The +// cadence index of each level is unchanged (leaf distance), only the presentation order moved. +test fn the_task_level_closes_every_due_set() -> Bool { let due = alignment_levels_due(chain: four_level_resolved(), turns_elapsed: 2000, state: base_state()) count(due) == 3 - && match due.first() { + && match due.last() { Present { value: TaskContractLevel { link } } => link.node_id == "task-a" _ => false } + && match due.first() { + Present { value: ProjectIntentLevel { link } } => link.node_id == "arc-mid" + _ => false + } } // THE ADAPTIVE STATE REPLACES THE BASE INTERVAL: two MISALIGNED steps moved the first project @@ -578,6 +603,8 @@ test fn a_refused_chain_resolves_unresolved_naming_the_fork_by_its_dependents() && (map(walked, l => l.node_id)) == ["arc-one", "arc-mid"] AlignmentNestingCyclic { node_id: _ } => false AlignmentRowUndeclared { node_id: _ } => false + AlignmentWalkUnrooted { at: _, walked: _ } => false + AlignmentEdgeContributionRefused { member: _, project: _, detail: _ } => false } AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false } @@ -596,6 +623,8 @@ test fn an_undeclared_referenced_row_resolves_unresolved_refused_by_name() -> Bo AlignmentRowUndeclared { node_id } => node_id == "arc-missing" AlignmentNestingCyclic { node_id: _ } => false AlignmentChainForked { at: _, dependents: _, walked: _ } => false + AlignmentWalkUnrooted { at: _, walked: _ } => false + AlignmentEdgeContributionRefused { member: _, project: _, detail: _ } => false } AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false } @@ -662,24 +691,26 @@ test fn the_rendered_prompt_carries_every_level_from_the_node_to_the_root_in_ord AlignmentReady { chain: _, due_levels: _, rendered_prompt } => string_contains(s: rendered_prompt, pattern: continuation_alignment_l0_pointer) && string_contains(s: rendered_prompt, pattern: continuation_alignment_ask) - && string_contains(s: rendered_prompt, pattern: "this task serves project arc-one: Arc One headline -- boundary: Arc One boundary") - && string_contains(s: rendered_prompt, pattern: "this level serves project arc-mid: Arc Mid headline -- boundary: Arc Mid boundary") - && string_contains(s: rendered_prompt, pattern: "this level serves project arc-two: Arc Two headline -- boundary: Arc Two boundary") - && string_contains(s: rendered_prompt, pattern: "L2 task contract: task-a: Task A headline -- boundary: Task A boundary") - && render_lists_ids_in_order(rendered: rendered_prompt, ordered_ids: ["arc-one", "arc-mid", "arc-two", "task-a"]) + && string_contains(s: rendered_prompt, pattern: "L1 project intent: arc-two: Arc Two headline") + && string_contains(s: rendered_prompt, pattern: "L2 project intent: arc-mid: Arc Mid headline") + && string_contains(s: rendered_prompt, pattern: "L3 project intent: arc-one: Arc One headline") + && string_contains(s: rendered_prompt, pattern: "L4 task contract: task-a: Task A headline") + && string_contains(s: rendered_prompt, pattern: "red control: fixture red control") + && render_lists_ids_in_order(rendered: rendered_prompt, ordered_ids: ["arc-two", "arc-mid", "arc-one", "task-a"]) AlignmentUnresolved { cause: _ } => false } } -test fn a_projectless_chain_renders_its_own_row_as_the_intent() -> Bool { - let standalone = AlignmentResolvedChain { - task: AlignmentChainLink { node_id: "lone-task", purpose: "Lone headline", boundary: "Lone boundary" }, +test fn the_root_projects_own_chain_renders_its_row_as_the_top_level() -> Bool { + let root_chain = AlignmentResolvedChain { + task: AlignmentChainLink { node_id: "gunbc-project-root", purpose: "Root project headline", boundary: "Root project boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, projects: [], root: alignment_root_pointer_fixture(), } - match alignment_resolve_chain(chain: AlignmentChained { task: standalone.task, projects: [], root: standalone.root }, turns_elapsed: 1, state: alignment_base_cadence_state()) { + match alignment_resolve_chain(chain: AlignmentChained { task: root_chain.task, projects: [], root: root_chain.root }, turns_elapsed: 1, state: alignment_base_cadence_state()) { AlignmentReady { chain: _, due_levels: _, rendered_prompt } => - string_contains(s: rendered_prompt, pattern: "this node is a project of one, so its own declared row is the intent: Lone headline -- boundary: Lone boundary") + string_contains(s: rendered_prompt, pattern: "L1 task contract: gunbc-project-root: Root project headline") + && string_contains(s: rendered_prompt, pattern: "none -- this level is the admitted root project") AlignmentUnresolved { cause: _ } => false } } @@ -691,20 +722,20 @@ test fn a_projectless_chain_renders_its_own_row_as_the_intent() -> Bool { test fn the_chain_fingerprint_moves_when_any_link_moves() -> Bool { let base = alignment_chain_fingerprint(chain: four_level_resolved()) let retitled = alignment_chain_fingerprint(chain: AlignmentResolvedChain { - task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary" }, + task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, projects: [ - AlignmentChainLink { node_id: "arc-one", purpose: "Arc One retitled", boundary: "Arc One boundary" }, - AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary" }, - AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary" }, + AlignmentChainLink { node_id: "arc-one", purpose: "Arc One retitled", boundary: "Arc One boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, ], root: alignment_root_pointer_fixture(), }) let reordered = alignment_chain_fingerprint(chain: AlignmentResolvedChain { - task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary" }, + task: AlignmentChainLink { node_id: "task-a", purpose: "Task A headline", boundary: "Task A boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, projects: [ - AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary" }, - AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary" }, - AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary" }, + AlignmentChainLink { node_id: "arc-two", purpose: "Arc Two headline", boundary: "Arc Two boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-mid", purpose: "Arc Mid headline", boundary: "Arc Mid boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, + AlignmentChainLink { node_id: "arc-one", purpose: "Arc One headline", boundary: "Arc One boundary", current_approach: "fixture current state", red_control: "fixture red control", handback: "fixture handback", displaced_cost: "fixture displaced cost", contribution_to_parent: "" }, ], root: alignment_root_pointer_fixture(), }) @@ -721,28 +752,64 @@ test fn the_chain_fingerprint_moves_when_any_link_moves() -> Bool { // named, and reaches no worker-start effect downstream). test fn production_resolution_has_no_fabricated_dependency_parents() -> Bool { match alignment_resolve(node_id: "shell-dag-cron-entry-line-builder", turns_elapsed: 1, state: alignment_base_cadence_state()) { - AlignmentReady { chain, due_levels: _, rendered_prompt: _ } => map(chain.projects, p => p.node_id) == ["shell-typed-invocation"] + AlignmentReady { chain, due_levels: _, rendered_prompt: _ } => map(chain.projects, p => p.node_id) == ["shell-typed-invocation", "gunbc-project-root"] _ => false } } -// A CLEAN PRODUCTION CHAIN RESOLVES READY, EVERY LEVEL DERIVED: the corpus's real rows climb two -// live arcs — extended-admission-cardinality serves accepted-extended-obligation-closure, which -// serves guarantee-residual-prevalence, a standalone root — and the render carries each derived -// level: the positive inhabitance claim pairing with the fork refusal above. (The roadmap-runtime -// rows refuse instead: their chain walks through superseded rows, and the walk names the halt — -// both answers are the one authority reading the live authority.) -test fn a_clean_production_chain_resolves_ready_rendering_every_derived_level() -> Bool { +// A PRODUCTION NODE WHOSE WALK NEVER REACHES THE ADMITTED ROOT REFUSES, naming the halt (review +// 5354823380): extended-admission-cardinality has no membership arc and is not a declared root, +// so its alignment resolution is Unresolved with the unrooted cause -- the RED control for the +// rooted-hierarchy rule, read off the live authority. +// +// Lane B (review 5358022153, 2026-09-30) rooted the whole dispatchable population and LEFT THIS +// NODE UNROOTED ON PURPOSE: it is the named refusal subject for the unrooted controls in this +// witness and in roadmap_dispatch_actuator_witness_test, and a witness whose red arm can never +// fire is not a control. It carries ExecutionContractUnspecified, so it sits outside the +// dispatchable population every_dispatchable_node_reaches_the_admitted_root covers. +test fn an_unrooted_production_node_refuses_naming_where_the_walk_halted() -> Bool { match alignment_resolve(node_id: "extended-admission-cardinality", turns_elapsed: 1, state: alignment_base_cadence_state()) { - AlignmentReady { chain, due_levels: _, rendered_prompt } => - chain.task.node_id == "extended-admission-cardinality" - && count(chain.projects) == 0 - && string_contains(s: rendered_prompt, pattern: "extended-admission-cardinality") - && alignment_chain_fingerprint(chain: chain) != "" - AlignmentUnresolved { cause: _ } => false + AlignmentUnresolved { cause } => + match cause { + AlignmentWalkUnrooted { at, walked: _ } => + at == "extended-admission-cardinality" + && string_contains(s: alignment_refusal_reason(cause: cause), pattern: "extended-admission-cardinality") + AlignmentChainForked { at: _, dependents: _, walked: _ } => false + AlignmentEdgeContributionRefused { member: _, project: _, detail: _ } => false + AlignmentRowUndeclared { node_id: _ } => false + AlignmentNestingCyclic { node_id: _ } => false + } + AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false } } +// EVERY DISPATCHABLE NODE REACHES THE ADMITTED ROOT (review 5358022153 lane B, 2026-09-30): the +// dispatchable population -- every node in declared_roadmap_nodes() whose execution is an +// ExecutionContract, derived here, never transcribed -- must derive AlignmentChained, because the +// dispatch actuator refuses to spawn on any other resolution. This claim has NO named exclusions: +// the deliberately unrooted control subject above carries ExecutionContractUnspecified and is not +// a member of this population, and every ExecutionContract row was attached to the hierarchy in +// the same change (the seven program-level parents live in roadmap_authority). A future node bound +// to a contract without a membership arc reds this claim exactly as its author needs it to. +fn dispatchable_node_ids() -> List { + declared_roadmap_nodes() + |> filter(n => match n.execution { + ExecutionContract { validations: _, extra_capabilities: _, path_scope: _, source_roots: _ } => true + ExecutionContractUnspecified => false + NotAgentWork { mode: _ } => false + }) + |> map(n => n.node as String) +} + +test fn every_dispatchable_node_reaches_the_admitted_root() -> Bool { + fold(dispatchable_node_ids(), init: true, f: fn(ok, id) { + ok && match alignment_chain_for(node_id: id) { + AlignmentChained { task: _, projects: _, root: _ } => true + _ => false + } + }) +} + // ------------------------------------------------------------------------------------------------ // The attempt binding codec: the chain fingerprint bound to the attempt identity, on the // submission/receipt idiom — schema wall, typed decode envelope, text round-trip. @@ -785,8 +852,70 @@ test fn a_binding_without_the_schema_wall_refuses() -> Bool { } test fn cyclic_nesting_refuses_instead_of_truncating_the_alignment_chain() -> Bool { - match alignment_chain(node_id: "task-a", memberships: [RoadmapMembership { member: "task-a", project: "arc-one" }, RoadmapMembership { member: "arc-one", project: "task-a" }], nodes: fixture_nodes()) { + match alignment_chain(node_id: "task-a", memberships: [RoadmapMembership { member: "task-a", project: "arc-one", contribution: "fixture" }, RoadmapMembership { member: "arc-one", project: "task-a", contribution: "fixture" }], nodes: fixture_nodes()) { AlignmentChainCyclic { node_id } => node_id == "task-a" _ => false } } + +// EVERY CARRIED FIELD MOVES THE FINGERPRINT (review 5358022153): a field that claims to define +// alignment but does not move the digest is a silent context channel. One mutation per field, each +// must change the fingerprint. +fn link_with(node_id: String, field: String, value: String) -> AlignmentChainLink { + let base = AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: "A", red_control: "R", handback: "H", displaced_cost: "D", contribution_to_parent: "C" } + if field == "purpose" { + AlignmentChainLink { node_id: node_id, purpose: value, boundary: "B", current_approach: "A", red_control: "R", handback: "H", displaced_cost: "D", contribution_to_parent: "C" } + } else if field == "boundary" { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: value, current_approach: "A", red_control: "R", handback: "H", displaced_cost: "D", contribution_to_parent: "C" } + } else if field == "current_approach" { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: value, red_control: "R", handback: "H", displaced_cost: "D", contribution_to_parent: "C" } + } else if field == "red_control" { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: "A", red_control: value, handback: "H", displaced_cost: "D", contribution_to_parent: "C" } + } else if field == "handback" { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: "A", red_control: "R", handback: value, displaced_cost: "D", contribution_to_parent: "C" } + } else if field == "displaced_cost" { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: "A", red_control: "R", handback: "H", displaced_cost: value, contribution_to_parent: "C" } + } else { + AlignmentChainLink { node_id: node_id, purpose: "P", boundary: "B", current_approach: "A", red_control: "R", handback: "H", displaced_cost: "D", contribution_to_parent: value } + } +} + +fn chain_with_task(link: AlignmentChainLink) -> AlignmentResolvedChain { + AlignmentResolvedChain { task: link, projects: [], root: alignment_root_pointer_fixture() } +} + +test fn every_carried_field_moves_the_fingerprint() -> Bool { + let base = alignment_chain_fingerprint(chain: chain_with_task(link: link_with(node_id: "t", field: "", value: ""))) + fold(["purpose", "boundary", "current_approach", "red_control", "handback", "displaced_cost", "contribution_to_parent"], init: true, f: fn(ok, field) { + ok && base != alignment_chain_fingerprint(chain: chain_with_task(link: link_with(node_id: "t", field: field, value: "MUTATED"))) + }) +} + +// AN EMPTY CONTRIBUTION ON A NON-ROOT EDGE REFUSES (review 5358022153): the empty string used to +// read as "this level is the admitted root". Now the walk reaches arc-one's link, its edge carries +// no argument, and the chain refuses with the edge named. +test fn an_empty_contribution_on_a_non_root_edge_refuses() -> Bool { + let edges = [ + RoadmapMembership { member: "task-a", project: "arc-one", contribution: "" }, + RoadmapMembership { member: "arc-one", project: "gunbc-project-root", contribution: "arc one contributes" }, + ] + match alignment_chain(node_id: "task-a", memberships: edges, nodes: fixture_nodes()) { + AlignmentChainContributionRefused { member, project, detail: _ } => + member == "task-a" && project == "arc-one" + _ => false + } +} + +test fn conflicting_contributions_on_one_edge_refuse() -> Bool { + let edges = [ + RoadmapMembership { member: "task-a", project: "arc-one", contribution: "one argument" }, + RoadmapMembership { member: "task-a", project: "arc-one", contribution: "a different argument" }, + RoadmapMembership { member: "arc-one", project: "gunbc-project-root", contribution: "arc one contributes" }, + ] + match alignment_chain(node_id: "task-a", memberships: edges, nodes: fixture_nodes()) { + AlignmentChainContributionRefused { member, project, detail: _ } => + member == "task-a" && project == "arc-one" + _ => false + } +} + diff --git a/dag/test/claim/roadmap/roadmap_attempt_continuation_witness_test.dag b/dag/test/claim/roadmap/roadmap_attempt_continuation_witness_test.dag index 093260bc06c..8e4b99952b5 100644 --- a/dag/test/claim/roadmap/roadmap_attempt_continuation_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_attempt_continuation_witness_test.dag @@ -32,7 +32,7 @@ import gunbc.roadmap_workflow_stage { } fn te(child: String, parent: String) -> RoadmapMembership { - RoadmapMembership { project: child, member: parent } + RoadmapMembership { project: child, member: parent, contribution: "fixture contribution" } } fn pri( diff --git a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag index af7293f644d..0a7d8a39f0d 100644 --- a/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_belt_actuate_witness_test.dag @@ -2626,11 +2626,16 @@ test fn the_checkpoint_lines_project_the_capture_record_verbatim() -> Bool { fn belt_test_ready_resolution(node_id: String, project_id: String) -> AlignmentResolution { alignment_resolve_chain( chain: AlignmentChained { - task: AlignmentChainLink { node_id: node_id, purpose: "Binding purpose", boundary: "Binding boundary" }, + task: AlignmentChainLink { node_id: node_id, purpose: "Binding purpose", boundary: "Binding boundary", current_approach: "fixture approach", red_control: "fixture red", handback: "fixture handback", displaced_cost: "fixture cost", contribution_to_parent: "fixture contribution" }, projects: [AlignmentChainLink { node_id: project_id, purpose: join(["Purpose of ", project_id], ""), boundary: join(["Boundary of ", project_id], ""), + current_approach: "fixture approach", + red_control: "fixture red", + handback: "fixture handback", + displaced_cost: "fixture cost", + contribution_to_parent: "fixture contribution", }], root: decl_ref(module_path: "gunbc.design_document", decl_name: "design_document"), }, diff --git a/dag/test/claim/roadmap/roadmap_checkpoint_plan_witness_test.dag b/dag/test/claim/roadmap/roadmap_checkpoint_plan_witness_test.dag new file mode 100644 index 00000000000..b6b3b370ce0 --- /dev/null +++ b/dag/test/claim/roadmap/roadmap_checkpoint_plan_witness_test.dag @@ -0,0 +1,263 @@ +module test.claim.roadmap.roadmap_checkpoint_plan_witness_test + +import std.types { Bool, String, List } +import std.decl_ref { decl_ref } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import gunbc.roadmap.roadmap_alignment { + AlignmentChainLink, + AlignmentResolvedChain, + AlignmentCadenceState, + AlignmentCheckpointIdentity, + alignment_checkpoint_identity_digest, + alignment_chain_fingerprint, + alignment_base_cadence_state, + Aligned, + Misaligned, + ExtremelyMisaligned, + CatastrophicallyMisaligned, + RoutedConflict, +} +import gunbc.roadmap.roadmap_checkpoint_plan { + AlignmentProbeLevel, + ProbePrinciples, + ProbeProject, + ProbeTask, + AlignmentProbeObligation, + ProbePlanDerived, + ProbePlanRefused, + checkpoint_probe_obligations, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// ------------------------------------------------------------------------------------------------ +// The checkpoint-probe planner, witnessed (review 5358022153, lane C). The claims discriminate +// one interface: checkpoint_probe_obligations' derivation over (checkpoint identity, resolved +// chain, cadence state) -- the obligation population's shape and root-to-leaf order, the +// idempotency of the obligation keys, the fingerprint gate, and the outstanding-guidance read of +// the cadence state's recorded verdicts. The fixture chains are supplied at the module boundary +// like the alignment witness's: the planner's domain is the RESOLVED chain, so the claims pass +// resolved records directly and never re-derive a walk. +// ------------------------------------------------------------------------------------------------ + +fn witness_link(node_id: String, purpose: String) -> AlignmentChainLink { + AlignmentChainLink { + node_id: node_id, + purpose: purpose, + boundary: join([purpose, " boundary"], ""), + current_approach: "fixture current approach", + red_control: "fixture red control", + handback: "fixture handback", + displaced_cost: "fixture displaced cost", + contribution_to_parent: join([node_id, " contribution"], ""), + } +} + +// THE THREE-LEVEL FIXTURE CHAIN: one task under one project under the root principles -- the +// smallest ladder that still exercises every probe kind (principles, project, task). +fn three_level_chain() -> AlignmentResolvedChain { + AlignmentResolvedChain { + task: witness_link(node_id: "task-a", purpose: "Task A headline"), + projects: [witness_link(node_id: "arc-one", purpose: "Arc One headline")], + root: decl_ref(module_path: "gunbc.design_document", decl_name: "design_document"), + } +} + +// THE MOVED CHAIN: same ids, one retitled purpose. Every carried field moves the chain +// fingerprint (the alignment witness pins that per field), so this is a genuinely moved chain +// under the same node ids -- the fingerprint gate's negative control. +fn moved_three_level_chain() -> AlignmentResolvedChain { + AlignmentResolvedChain { + task: witness_link(node_id: "task-a", purpose: "Task A headline"), + projects: [witness_link(node_id: "arc-one", purpose: "Arc One headline, retitled")], + root: decl_ref(module_path: "gunbc.design_document", decl_name: "design_document"), + } +} + +// THE FIXTURE CHECKPOINT, minted under the chain it is asked to plan for: the fingerprint gate +// passes by construction, so the derivation claims isolate the population itself. +fn fixture_checkpoint(chain: AlignmentResolvedChain) -> AlignmentCheckpointIdentity { + AlignmentCheckpointIdentity { + attempt_lineage: "worker:task-a-lineage", + epoch: 3, + checkpoint_ordinal: 2, + chain_fingerprint: alignment_chain_fingerprint(chain: chain), + conversation_tip: "conversation-tip-0001", + candidate_subject: "task-a", + } +} + +fn plan_level_kind(level: AlignmentProbeLevel) -> String { + match level { + ProbePrinciples => "principles" + ProbeProject { link: _ } => "project" + ProbeTask { link: _ } => "task" + } +} + +fn plan_level_subject(level: AlignmentProbeLevel) -> String { + match level { + ProbePrinciples => "" + ProbeProject { link } => link.node_id + ProbeTask { link } => link.node_id + } +} + +fn obligation_keys(obligations: List) -> List { + map(obligations, o => o.obligation_key) +} + +// THE GUIDANCE AS ONE COMPARABLE TEXT per obligation: joined so the claims read the population's +// guidance as a list of strings without nesting list equality through the match. +fn obligation_guidance_texts(obligations: List) -> List { + map(obligations, o => join(o.outstanding_guidance, "|")) +} + +// ALL DISTINCT, the population-key claim's helper: every key appears exactly once, so no two +// obligations of one checkpoint share a key. +fn all_distinct(items: List) -> Bool { + fold(items, init: true, f: fn(ok, item) { + ok && count(filter(items, other => other == item)) == 1 + }) +} + +// ONE CHECKPOINT OVER THE THREE-LEVEL CHAIN derives exactly three obligations, ROOT TO LEAF -- +// principles, project, task -- each obligation carrying the checkpoint it was minted under, each +// key distinct, and the plan's checkpoint_digest is the checkpoint identity's own digest, the +// idempotency root the scheduler keys its durable records on. +test fn a_checkpoint_over_a_three_level_chain_derives_three_obligations_root_to_leaf() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let fingerprint = alignment_chain_fingerprint(chain: chain) + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: alignment_base_cadence_state()) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest, obligations } => + checkpoint_digest == alignment_checkpoint_identity_digest(id: checkpoint) + && count(obligations) == 3 + && map(obligations, o => plan_level_kind(level: o.level)) == ["principles", "project", "task"] + && map(obligations, o => plan_level_subject(level: o.level)) == ["", "arc-one", "task-a"] + && map(obligations, o => o.checkpoint.chain_fingerprint) == [fingerprint, fingerprint, fingerprint] + && all_distinct(items: obligation_keys(obligations: obligations)) + } +} + +// IDEMPOTENT RE-DERIVATION, the red control: the same checkpoint identity over the same chain and +// state yields the identical obligation keys and guidance -- a crash after the plan derived but +// before the scheduler recorded it re-derives exactly the same population, so the durable records +// key on the same obligations and no duplicate verdicts can be minted for one checkpoint. +test fn re_deriving_under_the_same_checkpoint_identity_yields_identical_obligation_keys() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let state = AlignmentCadenceState { + level_intervals: [20, 200, 2000], + last_verdicts: [Misaligned { direction: "task drift" }, Aligned, Aligned], + } + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: state) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest: _, obligations: first } => + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: state) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest: _, obligations: second } => + obligation_keys(obligations: first) == obligation_keys(obligations: second) + && obligation_guidance_texts(obligations: first) == obligation_guidance_texts(obligations: second) + && map(first, o => plan_level_kind(level: o.level)) == map(second, o => plan_level_kind(level: o.level)) + } + } +} + +// THE FINGERPRINT GATE, negative control: a checkpoint whose recorded chain_fingerprint is not +// the resolved chain's current fingerprint refuses -- a typed refusal naming both fingerprints, +// never a derived population under a stale checkpoint. +test fn a_checkpoint_whose_fingerprint_does_not_match_the_chain_refuses() -> Bool { + let chain = three_level_chain() + let stale = AlignmentCheckpointIdentity { + attempt_lineage: "worker:task-a-lineage", + epoch: 3, + checkpoint_ordinal: 2, + chain_fingerprint: "fingerprint-from-a-moved-chain", + conversation_tip: "conversation-tip-0001", + candidate_subject: "task-a", + } + match checkpoint_probe_obligations(checkpoint: stale, chain: chain, state: alignment_base_cadence_state()) { + ProbePlanRefused { detail } => + string_contains(s: detail, pattern: "fingerprint-from-a-moved-chain") + && string_contains(s: detail, pattern: alignment_chain_fingerprint(chain: chain)) + ProbePlanDerived { checkpoint_digest: _, obligations: _ } => false + } +} + +// OUTSTANDING GUIDANCE IS THE RECORDED VERDICT PAYLOAD, read at the level's leaf distance: for +// the three-level chain the task contract sits at leaf distance 0, the project at 1, the +// principles at 2 -- the same index alignment_levels_due reads -- so the state's last_verdicts +// apply task-first. A MISALIGNED direction and an EXTREMELY MISALIGNED direction surface as +// their levels' guidance; ALIGNED surfaces as none. +test fn outstanding_guidance_carries_the_recorded_verdict_payload_and_none_for_aligned() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let state = AlignmentCadenceState { + level_intervals: [20, 200, 2000], + last_verdicts: [ + Misaligned { direction: "the task contract drifted" }, + ExtremelyMisaligned { direction: "the project intent drifted" }, + Aligned, + ], + } + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: state) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest: _, obligations } => + obligation_guidance_texts(obligations: obligations) == ["", "the project intent drifted", "the task contract drifted"] + } +} + +// THE PAYLOAD EXTRACTION COVERS EVERY NON-ALIGNED ARM: CATASTROPHICALLY MISALIGNED surfaces its +// reason and a RoutedConflict its conflict -- the guidance is the verdict's own recorded detail, +// never a re-interpretation of it. +test fn guidance_extracts_the_reason_and_conflict_arms_too() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let state = AlignmentCadenceState { + level_intervals: [20, 200, 2000], + last_verdicts: [ + CatastrophicallyMisaligned { reason: "the diff violates a root principle" }, + RoutedConflict { conflict: "the brief contradicts the contract" }, + Aligned, + ], + } + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: state) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest: _, obligations } => + obligation_guidance_texts(obligations: obligations) == ["", "the brief contradicts the contract", "the diff violates a root principle"] + } +} + +// A STATE THAT PREDATES A LEVEL leaves that level's guidance empty: one recorded verdict (the +// task's, at leaf distance 0) reads for the task alone; the project and principles levels were +// never judged, and an unrecorded level carries no fabricated guidance. +test fn a_level_the_state_has_not_reached_carries_no_guidance() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let partial = AlignmentCadenceState { + level_intervals: [20], + last_verdicts: [Misaligned { direction: "early task drift" }], + } + match checkpoint_probe_obligations(checkpoint: checkpoint, chain: chain, state: partial) { + ProbePlanRefused { detail: _ } => false + ProbePlanDerived { checkpoint_digest: _, obligations } => + obligation_guidance_texts(obligations: obligations) == ["", "", "early task drift"] + } +} + +// THE SAME CHECKPOINT IDENTITY OVER A MOVED CHAIN refuses, never derives: the checkpoint was +// minted under the original chain's fingerprint; the retitled chain derives a different +// fingerprint, and the plan refuses rather than minting obligations the stale checkpoint would +// execute against links it never saw. +test fn the_same_checkpoint_identity_over_a_moved_chain_refuses_never_derives() -> Bool { + let chain = three_level_chain() + let checkpoint = fixture_checkpoint(chain: chain) + let moved = moved_three_level_chain() + alignment_chain_fingerprint(chain: moved) != checkpoint.chain_fingerprint + && match checkpoint_probe_obligations(checkpoint: checkpoint, chain: moved, state: alignment_base_cadence_state()) { + ProbePlanRefused { detail } => detail != "" + ProbePlanDerived { checkpoint_digest: _, obligations: _ } => false + } +} diff --git a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag index fa939897cb9..81f33167ec5 100644 --- a/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_dispatch_actuator_witness_test.dag @@ -166,6 +166,7 @@ import gunbc.roadmap.roadmap_attempt_continuation { import gunbc.roadmap.roadmap_alignment { AlignmentResolution, AlignmentReady, AlignmentUnresolved, AlignmentChained, AlignmentChainLink, AlignmentChainForked, AlignmentRowUndeclared, + AlignmentWalkUnrooted, AlignmentEdgeContributionRefused, AlignmentNestingCyclic, alignment_resolve, alignment_resolve_chain, alignment_base_cadence_state, alignment_refusal_reason, } import std.decl_ref { decl_ref } @@ -210,11 +211,16 @@ fn fixture_doc(nodes: List, edges: List) -> RoadmapDoc fn fixture_ready_resolution(node_id: String, project_ids: List) -> AlignmentResolution { alignment_resolve_chain( chain: AlignmentChained { - task: AlignmentChainLink { node_id: node_id, purpose: "Fixture purpose", boundary: "Fixture boundary" }, + task: AlignmentChainLink { node_id: node_id, purpose: "Fixture purpose", boundary: "Fixture boundary", current_approach: "fixture approach", red_control: "fixture red", handback: "fixture handback", displaced_cost: "fixture cost", contribution_to_parent: "fixture contribution" }, projects: map(project_ids, pid => AlignmentChainLink { node_id: pid, purpose: join(["Purpose of ", pid], ""), boundary: join(["Boundary of ", pid], ""), + current_approach: "fixture approach", + red_control: "fixture red", + handback: "fixture handback", + displaced_cost: "fixture cost", + contribution_to_parent: "fixture contribution", }), root: decl_ref(module_path: "gunbc.design_document", decl_name: "design_document"), }, @@ -1221,11 +1227,11 @@ fn authority_node_opt(id: String) -> RoadmapNode? { // THE ONE-AUTHORITY PREAMBLE (review finding 2026-09-26): the preamble IS the resolution's // rendered_prompt — gunbc.roadmap.roadmap_alignment owns the alignment answer and this lane only -// concatenates. For the REAL node namespace-occurrence-transport the derived chain refuses one -// level up — namespace-binding-kernel has three declared dependents, so the honest preamble is the -// refusal marker naming the fork, never the confident single-level projection the old string -// helpers rendered while execution proceeded. -test fn the_preamble_names_the_derived_refusal_for_a_real_forked_node() -> Bool { +// concatenates. For the REAL node namespace-occurrence-transport the derived chain now refuses +// UNROOTED — it holds no membership arc and is not the admitted root, so the honest preamble is +// the refusal marker naming where the walk halted (review 5354823380), never a confident +// projection rendered while execution proceeded. +test fn the_preamble_names_the_derived_refusal_for_an_unrooted_node() -> Bool { let resolution = alignment_resolve( node_id: "namespace-occurrence-transport", turns_elapsed: 2, @@ -1236,37 +1242,43 @@ test fn the_preamble_names_the_derived_refusal_for_a_real_forked_node() -> Bool AlignmentUnresolved { cause } => preamble.contains("Alignment UNRESOLVED") && (match cause { - AlignmentChainForked { at, dependents, walked: _ } => - at == "namespace-binding-kernel" && count(dependents) == 3 + AlignmentWalkUnrooted { at, walked } => + at == "namespace-occurrence-transport" && count(walked) == 0 + AlignmentChainForked { at: _, dependents: _, walked: _ } => false + AlignmentEdgeContributionRefused { member: _, project: _, detail: _ } => false AlignmentRowUndeclared { node_id: _ } => false + AlignmentNestingCyclic { node_id: _ } => false }) - && string_contains(s: preamble, pattern: "namespace-binding-kernel") - && !preamble.contains("this task serves project") + && string_contains(s: preamble, pattern: "namespace-occurrence-transport") AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false } } -// A RESOLVED CHAIN RENDERS EVERY LEVEL NODE TO ROOT through the same preamble: the L0 pointer, the -// nearest project with its carried contribution, deeper levels above it, the task contract, and -// the bounded ask — not just the immediate L1 the deleted projection stopped at. +// A RESOLVED CHAIN RENDERS EVERY LEVEL ROOT TO LEAF through the same preamble (review +// 5354823380): the L0 pointer, the root-most project first, the nearer project below it, the task +// contract last, and the bounded ask — each level carrying its ticket's approach and red control, +// not a two-field reduction. test fn the_preamble_renders_every_chain_level_when_the_resolution_is_ready() -> Bool { let preamble = dispatch_continuation_alignment_preamble( resolution: fixture_ready_resolution(node_id: "ts-alignment-deep", project_ids: ["arc-near", "arc-far"]), ) preamble.contains(continuation_alignment_l0_pointer) - && preamble.contains("this task serves project arc-near: Purpose of arc-near -- boundary: Boundary of arc-near") - && preamble.contains("L1 project intent (level 2 above the task): this level serves project arc-far: Purpose of arc-far -- boundary: Boundary of arc-far") - && preamble.contains("L2 task contract: ts-alignment-deep: Fixture purpose -- boundary: Fixture boundary") + && preamble.contains("L1 project intent: arc-far: Purpose of arc-far") + && preamble.contains("L2 project intent: arc-near: Purpose of arc-near") + && preamble.contains("L3 task contract: ts-alignment-deep: Fixture purpose") + && preamble.contains("approach: fixture approach") + && preamble.contains("red control: fixture red") && preamble.contains(continuation_alignment_ask) } -// A PROJECT-OF-ONE PROJECTS ITS OWN ROW: a node no arc depends on has no arc intent to serve, so -// its own declared headline and boundary are the L1 line. -test fn a_project_of_one_names_its_own_row_as_the_project_intent() -> Bool { +// A CHAIN WITH NO PROJECT LEVELS renders its task contract as the single level below the +// principles pointer — the shape the admitted root's own chain takes. +test fn a_projectless_ready_chain_renders_its_task_as_the_single_level() -> Bool { let preamble = dispatch_continuation_alignment_preamble( resolution: fixture_ready_resolution(node_id: "ts-alignment-project-of-one", project_ids: []), ) - preamble.contains("this node is a project of one, so its own declared row is the intent: Fixture purpose -- boundary: Fixture boundary") + preamble.contains("L1 task contract: ts-alignment-project-of-one: Fixture purpose") + && preamble.contains("contribution to parent: fixture contribution") } // THE INITIAL BRIEF IS UNCHANGED: the preamble exists only on continuation origins, the fresh @@ -1293,23 +1305,30 @@ test fn the_alignment_preamble_is_continuation_only_and_follows_the_verdict() -> && !fresh.contains("Alignment (re-checked") && cont.starts_with("CONTINUATION: this is turn 2 of 3") && cont.contains("verification failed: sandbox render red") - && cont.contains("\nAlignment (re-checked at every continuation turn):\n") + && cont.contains("\nAlignment (re-checked at every checkpoint adjudication):\n") && cont.contains(continuation_alignment_ask) && (match fresh_resolution { - AlignmentUnresolved { cause } => (match cause { AlignmentRowUndeclared { node_id } => node_id == "ts-alignment-fresh" AlignmentChainForked { at: _, dependents: _, walked: _ } => false }) + AlignmentUnresolved { cause } => (match cause { AlignmentRowUndeclared { node_id } => node_id == "ts-alignment-fresh" AlignmentChainForked { at: _, dependents: _, walked: _ } => false AlignmentWalkUnrooted { at: _, walked: _ } => false + AlignmentEdgeContributionRefused { member: _, project: _, detail: _ } => false AlignmentNestingCyclic { node_id: _ } => false }) AlignmentReady { chain: _, due_levels: _, rendered_prompt: _ } => false }) } // NO SPAWN ON AN UNRESOLVED CHAIN — THE REAL PRODUCER, THE REAL AUTHORITY (the review finding's -// discriminating control). The production node shell-dag-cron-entry-line-builder's chain refuses -// at the shell-gate-migration fork ON THE LIVE AUTHORITY; the same resolution the belt derives at -// spawn time is handed to the real spawn fold, and the fold's answer is DispatchSpawnRefused at -// the alignment gate — the Refused arm carries no DispatchSpawnCommands, so zero spawn commands -// exist: no worktree add, no state prepare, no session container. Execution cannot proceed on a -// chain the authority cannot read. +// discriminating control). The production node extended-admission-cardinality's chain refuses +// UNROOTED on the live authority (its membership walk never reaches the admitted root project, +// review 5354823380); the same resolution the belt derives at spawn time is handed to the real +// spawn fold, and the fold's answer is DispatchSpawnRefused at the alignment gate — the Refused +// arm carries no DispatchSpawnCommands, so zero spawn commands exist: no worktree add, no state +// prepare, no session container. Execution cannot proceed on a chain the authority cannot read. +// +// Lane B (review 5358022153, 2026-09-30) rooted every dispatchable node and LEFT THIS NODE +// UNROOTED ON PURPOSE: it is the named refusal subject keeping this control discriminating, and +// it carries ExecutionContractUnspecified, so dispatch would refuse it at launch admission long +// before alignment either way. Re-rooting it (or any future contract-bearing node) requires +// choosing a new unrooted subject for this claim first. test fn an_unresolved_alignment_refuses_the_real_spawn_fold_with_zero_commands() -> Bool { - match authority_node_opt(id: "shell-dag-cron-entry-line-builder") { + match authority_node_opt(id: "extended-admission-cardinality") { Absent => false Present { value: node } => match dispatch_spawn_commands_for_resolved_instance( @@ -1321,13 +1340,12 @@ test fn an_unresolved_alignment_refuses_the_real_spawn_fold_with_zero_commands() effort: ReasoningMedium, model: dispatch_codex_default_model, origin: FreshFromBase, - resolution: alignment_resolve(node_id: "shell-dag-cron-entry-line-builder", turns_elapsed: 1, state: alignment_base_cadence_state()), + resolution: alignment_resolve(node_id: "extended-admission-cardinality", turns_elapsed: 1, state: alignment_base_cadence_state()), ) { DispatchSpawnRefused { step, detail } => step as String == "alignment" - && string_contains(s: detail, pattern: "shell-gate-migration") - && string_contains(s: detail, pattern: "fleet-closed-loop-converge") - && string_contains(s: detail, pattern: "shell-residue-zero") + && string_contains(s: detail, pattern: "extended-admission-cardinality") + && string_contains(s: detail, pattern: "admitted root") DispatchSpawnReady { commands: _ } => false } } diff --git a/dag/test/claim/roadmap/roadmap_nesting_witness_test.dag b/dag/test/claim/roadmap/roadmap_nesting_witness_test.dag index 5fd50946438..d7ce57318f8 100644 --- a/dag/test/claim/roadmap/roadmap_nesting_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_nesting_witness_test.dag @@ -6,20 +6,20 @@ import gunbc.roadmap.roadmap_project { node_project_membership, project_member_i // The prerequisite is shared by two projects but belongs to neither by implication. test fn shared_prerequisite_does_not_assign_ownership() -> Bool { - let nesting = [RoadmapMembership { member: "task", project: "project-A" }] + let nesting = [RoadmapMembership { member: "task", project: "project-A", contribution: "fixture contribution" }] match node_project_membership(node_id: "shared-prerequisite", memberships: nesting) { MembershipStandalone => true _ => false } && project_member_ids(arc: "project-A", memberships: nesting) == ["task"] && project_member_ids(arc: "project-B", memberships: nesting) == [] } test fn nesting_is_recursive_without_type_labels() -> Bool { - let nesting = [RoadmapMembership { member: "task", project: "subproject" }, RoadmapMembership { member: "subproject", project: "root" }] + let nesting = [RoadmapMembership { member: "task", project: "subproject", contribution: "fixture contribution" }, RoadmapMembership { member: "subproject", project: "root", contribution: "fixture contribution" }] project_root_for_node(node_id: "task", memberships: nesting) == Present { value: "root" } && project_root_for_node(node_id: "subproject", memberships: nesting) == Present { value: "root" } && project_root_for_node(node_id: "root", memberships: nesting) == Present { value: "root" } } test fn cycles_and_multiple_parents_have_no_focus_root() -> Bool { - project_root_for_node(node_id: "a", memberships: [RoadmapMembership { member: "a", project: "b" }, RoadmapMembership { member: "b", project: "a" }]) == none - && project_root_for_node(node_id: "a", memberships: [RoadmapMembership { member: "a", project: "b" }, RoadmapMembership { member: "a", project: "c" }]) == none + project_root_for_node(node_id: "a", memberships: [RoadmapMembership { member: "a", project: "b", contribution: "fixture contribution" }, RoadmapMembership { member: "b", project: "a", contribution: "fixture contribution" }]) == none + && project_root_for_node(node_id: "a", memberships: [RoadmapMembership { member: "a", project: "b", contribution: "fixture contribution" }, RoadmapMembership { member: "a", project: "c", contribution: "fixture contribution" }]) == none } diff --git a/dag/test/claim/roadmap/roadmap_presentation_witness_test.dag b/dag/test/claim/roadmap/roadmap_presentation_witness_test.dag index 71ec6d01d26..a6c2748d3af 100644 --- a/dag/test/claim/roadmap/roadmap_presentation_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_presentation_witness_test.dag @@ -9,11 +9,24 @@ import extdeps.languages.json.emit { serialize_json } import gunbc.roadmap_model { RoadmapNode, RoadmapEdge, + RoadmapNodeId, RoadmapNodeIdentity, Pointer, Unsized, IntricacyLow, VolumeSmall, } import gunbc.roadmap_status { AuthoredLine, RedControlExecuted, HandbackDelivered, + TicketLine, TicketFields, Unsigned, +} +import gunbc.roadmap_execution_contract { ExecutionContractUnspecified } +import gunbc.roadmap.roadmap_alignment { + AlignmentResolution, + alignment_chain, alignment_resolve_chain, alignment_base_cadence_state, +} +import gunbc.roadmap_page { + IssueBreadcrumbPath, IssueBreadcrumbRefused, + IssueBreadcrumbProject, IssueBreadcrumbTask, + issue_breadcrumb_of, issue_alignment_breadcrumb_node, + project_suite_headline, project_suite_rows, project_suite_row_node, } import gunbc.roadmap_workflow_stage { WorkflowSegment, WorkflowSegmentState, @@ -1092,7 +1105,7 @@ test fn witness_projectless_active_row_renders_flat_in_active() -> Bool { test fn witness_shared_prerequisite_uses_only_authored_nesting() -> Bool { let nodes = [fx_sized(id: "hier-p1", content: "project one"), fx_sized(id: "hier-p2", content: "project two"), fx_sized(id: "hier-shared", content: "shared prerequisite")] let edges = [gunbc.roadmap_authority.edge(child: "hier-p2", parent: "hier-shared"), gunbc.roadmap_authority.edge(child: "hier-p1", parent: "hier-shared")] - let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-shared", project: "hier-p1" }] + let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-shared", project: "hier-p1", contribution: "fixture contribution" }] let rows = map(nodes, rn => fx_hier_view(rn: rn, nodes: nodes, edges: edges, merged: [], activity: NoActiveWork, standing: none)) row_display_parent_id(rows: rows, edges: edges, members: [], node_id: "hier-shared", memberships: nesting) == Present { value: "hier-p1" } && row_display_parent_id(rows: rows, edges: edges, members: [], node_id: "hier-shared", memberships: []) == none @@ -1267,7 +1280,7 @@ test fn witness_shared_project_child_collapses_only_under_owner() -> Bool { gunbc.roadmap_authority.edge(child: "hier-sp-p1", parent: "hier-sp-m"), gunbc.roadmap_authority.edge(child: "hier-sp-m", parent: "hier-sp-t"), ] - let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-m", project: "hier-sp-p2" }, gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-t", project: "hier-sp-m" }] + let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-m", project: "hier-sp-p2", contribution: "fixture contribution" }, gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-t", project: "hier-sp-m", contribution: "fixture contribution" }] let rows = map(nodes, rn => fx_hier_view(rn: rn, nodes: nodes, edges: edges, merged: [], activity: NoActiveWork, standing: none)) let ws = daily_workspace_view(rows: rows, edges: edges, program: none, observation: ObservationsNominal, families: [], members: [], memberships: nesting) let ids = fx_ws_rendered_ids(ws: ws) @@ -1555,7 +1568,7 @@ test fn witness_non_primary_child_is_a_link_not_a_member() -> Bool { gunbc.roadmap_authority.edge(child: "hier-sp-p1", parent: "hier-sp-m"), gunbc.roadmap_authority.edge(child: "hier-sp-m", parent: "hier-sp-t"), ] - let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-m", project: "hier-sp-p2" }, gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-t", project: "hier-sp-m" }] + let nesting = [gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-m", project: "hier-sp-p2", contribution: "fixture contribution" }, gunbc.roadmap_nesting.RoadmapMembership { member: "hier-sp-t", project: "hier-sp-m", contribution: "fixture contribution" }] let rows = map(nodes, rn => fx_hier_view(rn: rn, nodes: nodes, edges: edges, merged: [], activity: NoActiveWork, standing: none)) let p1 = project_tree_view(row: fx_row_of(rows: rows, node_id: "hier-sp-p1"), rows: rows, edges: edges, members: [], depth: 1, fuel: count(rows), memberships: nesting) let p2 = project_tree_view(row: fx_row_of(rows: rows, node_id: "hier-sp-p2"), rows: rows, edges: edges, members: [], depth: 1, fuel: count(rows), memberships: nesting) @@ -1713,8 +1726,183 @@ test fn witness_pending_stage_names_what_it_waits_on() -> Bool { && issue_wait_state_label(state_key: "refused", wait_target: "workspace") == "refused" } +// ── Project path breadcrumb and project suite (operator request 2026-09-29) ─────────────────── +// +// The breadcrumb's ONLY derivation is the alignment resolution the dispatch gate consumes -- +// fixtures build that resolution through the same alignment_chain + alignment_resolve_chain +// crossing production names, over ticket rows (the chain projects ONLY from ticket rows). The +// chain must end at the admitted root "gunbc-project-root": declared_roadmap_roots is the one +// live read the walk makes. Every fixture is constructed; no live roadmap population is read. + +fn fx_aln_fields(headline: String, brief: String) -> TicketFields { + TicketFields { + headline: headline, + brief: brief, + displaced_cost: "fixture displaced cost", + current_state: "fixture current state", + first_slice: "fixture first slice", + red_control: "fixture red control", + out_of_scope: "fixture out of scope", + handback: "fixture handback", + updates: [], + authored_on: "", + last_verified_on: "", + } +} + +fn fx_aln_ticket(id: String, headline: String, brief: String) -> RoadmapNode { + RoadmapNode { + identity: join(["rnfx-", id], "") as RoadmapNodeIdentity, + node: id as RoadmapNodeId, + line: TicketLine { done: false, ticket: fx_aln_fields(headline: headline, brief: brief) }, + carriers: [] as List, + sizing: Unsized, + execution: ExecutionContractUnspecified, + owner: "presentation-fixture", + signoff: Unsigned, + } +} + +fn fx_aln_resolution(task: String, memberships: List, nodes: List) -> AlignmentResolution { + alignment_resolve_chain( + chain: alignment_chain(node_id: task, memberships: memberships, nodes: nodes), + turns_elapsed: 1, + state: alignment_base_cadence_state(), + ) +} + +fn fx_aln_rooted_nodes() -> List { + [ + fx_aln_ticket(id: "hier-aln-task", headline: "Task headline", brief: "Task boundary"), + fx_aln_ticket(id: "hier-aln-mid", headline: "Mid headline", brief: "Mid boundary"), + fx_aln_ticket(id: "gunbc-project-root", headline: "Root headline", brief: "Root boundary"), + ] +} + +fn fx_aln_rooted_memberships() -> List { + [ + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-aln-task", project: "hier-aln-mid", contribution: "task to mid" }, + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-aln-mid", project: "gunbc-project-root", contribution: "mid to root" }, + ] +} + +// THE READY ARM, ROOT-TO-LEAF: a rooted node's breadcrumb lists the root project first and the +// task last; the project crumbs carry /project/ links and their boundary rides the title +// attribute; the task crumb is the current page and renders as PLAIN TEXT (no /issue/ self-link +// anywhere in the nav); the root-principles pointer contributes no crumb at all. A crumb folded +// out of the raw memberships, a leaf-first order, or a partial path reds every conjunct. +test fn witness_issue_breadcrumb_renders_root_to_leaf_with_suite_links() -> Bool { + let resolution = fx_aln_resolution(task: "hier-aln-task", memberships: fx_aln_rooted_memberships(), nodes: fx_aln_rooted_nodes()) + let structural = match issue_breadcrumb_of(resolution: resolution) { + IssueBreadcrumbPath { crumbs } => + map(crumbs, c => match c { + IssueBreadcrumbProject { node_id, purpose: _, boundary: _ } => node_id + IssueBreadcrumbTask { node_id, purpose: _, boundary: _ } => node_id + }) == ["gunbc-project-root", "hier-aln-mid", "hier-aln-task"] + && (match crumbs.first() { + Present { value: c } => match c { + IssueBreadcrumbProject { node_id: _, purpose: _, boundary: _ } => true + IssueBreadcrumbTask { node_id: _, purpose: _, boundary: _ } => false + } + Absent => false + }) + && (match crumbs.last() { + Present { value: c } => match c { + IssueBreadcrumbTask { node_id: _, purpose: _, boundary: _ } => true + IssueBreadcrumbProject { node_id: _, purpose: _, boundary: _ } => false + } + Absent => false + }) + IssueBreadcrumbRefused { reason: _ } => false + } + let rendered = match try_serialize_html_source(node: issue_alignment_breadcrumb_node(resolution: resolution)) { + EmitRejected { reason: _ } => false + EmitOk { html: h } => + string_contains(s: h, pattern: "href=\"/project/gunbc-project-root\"") + && string_contains(s: h, pattern: "href=\"/project/hier-aln-mid\"") + && string_contains(s: h, pattern: "Root headline") + && string_contains(s: h, pattern: "Mid headline") + && string_contains(s: h, pattern: "Task headline") + && string_contains(s: h, pattern: " › ") + && string_contains(s: h, pattern: "title=\"Root boundary\"") + && !string_contains(s: h, pattern: "href=\"/issue/") + && !string_contains(s: h, pattern: "principles") + } + structural && rendered +} + +// THE REFUSAL ARM, NO PARTIAL PATH: an unrooted node's breadcrumb renders "project path +// unavailable: " plus the typed reason, and NOT ONE crumb of the walked prefix -- no /project/ +// link, no headline text. Rendering the prefix as if it were a path is the red this control pins. +test fn witness_issue_breadcrumb_refusal_renders_no_partial_path() -> Bool { + let nodes = [fx_aln_ticket(id: "hier-aln-lone", headline: "Lone headline", brief: "Lone boundary")] + let resolution = fx_aln_resolution(task: "hier-aln-lone", memberships: [], nodes: nodes) + match issue_breadcrumb_of(resolution: resolution) { + IssueBreadcrumbRefused { reason } => + string_contains(s: reason, pattern: "hier-aln-lone") + && (match try_serialize_html_source(node: issue_alignment_breadcrumb_node(resolution: resolution)) { + EmitRejected { reason: _ } => false + EmitOk { html: h } => + string_contains(s: h, pattern: "project path unavailable: ") + && string_contains(s: h, pattern: "issue-breadcrumb-refused") + && !string_contains(s: h, pattern: "href=\"/project/") + && !string_contains(s: h, pattern: "Lone headline") + }) + IssueBreadcrumbPath { crumbs: _ } => false + } +} + +// THE SUITE LISTING'S MEMBERSHIP: exactly the DIRECT descendants -- the memberships authored +// under THIS project, nothing from another project -- one row each, the row titled from its node +// row when one exists and spelling the raw id when none does, and the row links to the member's +// /issue/ page. The heading projection reads ticket rows only: the authored (non-ticket) member +// row has no headline, while the ticket project row does. +test fn witness_project_suite_lists_exactly_the_direct_members() -> Bool { + let memberships = [ + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-suite-m1", project: "hier-suite-proj", contribution: "c1" }, + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-suite-m2", project: "hier-suite-proj", contribution: "c2" }, + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-suite-elsewhere", project: "hier-suite-other", contribution: "c3" }, + ] + let nodes = [ + fx_aln_ticket(id: "hier-suite-proj", headline: "suite project headline", brief: "project boundary"), + fx_sized(id: "hier-suite-m1", content: "member one title"), + ] + let rows = project_suite_rows(memberships: memberships, edges: [], nodes: nodes, project: "hier-suite-proj") + map(rows, r => r.node_id) == ["hier-suite-m1", "hier-suite-m2"] + && count(rows |> filter(r => r.node_id == "hier-suite-elsewhere")) == 0 + && (match rows.first() { + Present { value: r } => + r.title == "member one title" + && (match try_serialize_html_source(node: project_suite_row_node(row: r)) { + EmitOk { html: row_html } => string_contains(s: row_html, pattern: "href=\"/issue/hier-suite-m1\"") + EmitRejected { reason: _ } => false + }) + Absent => false + }) + && (match rows |> filter(r => r.node_id == "hier-suite-m2") |> first() { + Present { value: r } => r.title == "hier-suite-m2" + Absent => false + }) + && project_suite_headline(nodes: nodes, node_id: "hier-suite-proj") == Present { value: "suite project headline" } + && project_suite_headline(nodes: nodes, node_id: "hier-suite-m1") == none +} + +// THE SUITE LISTING'S TOPOLOGICAL LAYERING: a member whose in-set prerequisite is another member +// sorts AFTER that member even when authored BEFORE it -- authored order [m2, m1] with the edge +// making m1 a prerequisite of m2 renders m1 first; with no in-set edges at all the authored order +// stands. A declaration-order listing reds the first conjunct. +test fn witness_project_suite_topological_layering_orders_prereq_first() -> Bool { + let memberships = [ + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-suite-m2", project: "hier-suite-proj", contribution: "c2" }, + gunbc.roadmap_nesting.RoadmapMembership { member: "hier-suite-m1", project: "hier-suite-proj", contribution: "c1" }, + ] + let edges = [gunbc.roadmap_authority.edge(child: "hier-suite-m2", parent: "hier-suite-m1")] + map(project_suite_rows(memberships: memberships, edges: edges, nodes: [], project: "hier-suite-proj"), r => r.node_id) == ["hier-suite-m1", "hier-suite-m2"] + && map(project_suite_rows(memberships: memberships, edges: [], nodes: [], project: "hier-suite-proj"), r => r.node_id) == ["hier-suite-m2", "hier-suite-m1"] +} + // These older fixtures explicitly declare both relations with the same endpoints. Production // never derives ownership this way; the independent-relation control uses different endpoints. fn fixture_nesting_relation(edges: List) -> List { - map(edges, e => gunbc.roadmap_nesting.RoadmapMembership { member: e.parent as String, project: e.child as String }) + map(edges, e => gunbc.roadmap_nesting.RoadmapMembership { member: e.parent as String, project: e.child as String, contribution: "fixture contribution" }) } diff --git a/dag/test/claim/roadmap/roadmap_submission_witness_test.dag b/dag/test/claim/roadmap/roadmap_submission_witness_test.dag index 1a17ed323c8..89ad0c544a1 100644 --- a/dag/test/claim/roadmap/roadmap_submission_witness_test.dag +++ b/dag/test/claim/roadmap/roadmap_submission_witness_test.dag @@ -616,8 +616,8 @@ test fn the_integration_receipt_codec_round_trips_both_states() -> Bool { fn batch_memberships() -> List { [ - RoadmapMembership { project: "shell-arc", member: "task-one" }, - RoadmapMembership { project: "shell-arc", member: "task-two" }, + RoadmapMembership { project: "shell-arc", member: "task-one", contribution: "fixture contribution" }, + RoadmapMembership { project: "shell-arc", member: "task-two", contribution: "fixture contribution" }, ] } @@ -645,7 +645,7 @@ test fn a_node_with_no_dependent_arc_is_a_project_of_one_keyed_by_itself() -> Bo test fn two_arcs_over_one_node_is_a_typed_ambiguity_not_a_default() -> Bool { let edges = concat(batch_memberships(), [ - RoadmapMembership { project: "other-arc", member: "task-one" }, + RoadmapMembership { project: "other-arc", member: "task-one", contribution: "fixture contribution" }, ]) match node_project_membership(node_id: "task-one", memberships: edges) { MembershipAmbiguous { arcs } => count(arcs) == 2