Skip to content

feat(resource)!: NDO Layer 1 typed governance rules, classification constraints, and OperationalState - #132

Merged
Soushi888 merged 20 commits into
devfrom
ndo-layer1
Aug 29, 2026
Merged

Soushi888 merged 20 commits into
devfrom
ndo-layer1

Conversation

@Soushi888

@Soushi888 Soushi888 commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

Intent

Activates NDO Layer 1 (Specified) on top of the existing Layer 0 identity anchor, and closes two long-standing TODOs in the resource zome: the untyped GovernanceRule payload, and the ResourceState enum that conflated lifecycle maturity with operational condition.

The result is that a ResourceSpecification now points at the Layer 0 NondominiumIdentity it activates, governance rules carry a typed schema instead of a JSON blob, and classification coherence (regime x nature x rivalry x action) is enforced by pure predicates shared between the integrity zome, the coordinator, and the UI.

Branch authored by Tibi (ndo-layer1, 6 commits), then reviewed and hardened in four further commits (see Review follow-ups).

Changes

Shared crate (crates/shared)

  • New rule_data module: RuleData tagged enum with four variants (AccessRequirement, UsageLimit, TransferCondition, MaintenanceSchedule). GovernanceRuleType is derived from the discriminant via RuleData::rule_type(), never stored separately.
  • New constraints module: pure, hdk-free predicates over a ResourceClassification (nature, regime, lifecycle, rivalry override). Emits ConstraintViolation { rule_id, message, severity } with Hard / Soft severity. Current rules: nondominium_no_unilateral_capture, ownership_transfer_not_permitted_by_regime, gated_access_contradicts_permissionless_regime, no_transport_for_non_physical_nature.
  • New enums in types: Rivalry (with ResourceNature::default_rivalry()), ResourceScope (Project / Network / Public), OperationalState (Available, Reserved, InTransit, InStorage, InMaintenance, InUse, PendingValidation). New PropertyRegime predicates: is_rivalrous, permits_ownership_transfer, is_uncapturable, default_accessibility.

Zomes

  • ResourceSpecification gains scope, ndo_identity_hash (the immutable, stable Layer 0 pointer), and ndo_state_hash (the NDO action whose lifecycle stage the author observed). Both hashes are immutable after creation. Project-scoped specs skip the global discovery anchor.
  • GovernanceRule replaces rule_type: String + rule_data: String with a typed RuleData, plus ndo_identity_hash and denormalized property_regime / resource_nature / rivalry_override. Integrity reads the referenced Layer 0 record and rejects any classification that contradicts it (see Review follow-ups).
  • EconomicResource.state: ResourceState becomes operational_state: OperationalState; ResourceState is deleted. ResourcesByState becomes ResourcesByOperationalState; lifecycle faceting stays on Layer 0's NdoByLifecycleStage.
  • New NdoToSpecification link type: the Layer 0 to Layer 1 activation edge.
  • zome_gouvernance: new transition module, plus economic-event and commitment updates carrying the constraint context.

UI

  • New components: SpecificationCreateModal, RuleEditorModal (typed rule authoring), CommitmentCreateForm, EconomicEventCreateForm.
  • New helpers: operational-state-labels.ts, rivalry.ts. Governance / resource services, stores, and schemas updated for the new shapes.
  • GroupService stub replaced with a real callZome service layer for the Group DNA.
  • packages/shared-types carries the wire contract for all of the above: RuleData and its four variants, ResourceScope, Rivalry, OperationalState, and the new ResourceSpecification fields (+194 lines across resource.types.ts and governance.types.ts).

CI

  • New sweettest job: cargo test -p nondominium_shared plus all five Sweettest targets, sharded one job per [[test]] target with a shared rust-cache key. e2e is now gated behind it, as the workflow's own comment had asked for. Before this PR, the primary backend suite per CLAUDE.md had never gated a merge.

Tests

Sweettest coverage extended in resource, governance, ndo_layer0, and the group ndo_anchor suite (about 800 added lines across the four), plus six negative tests added during review.

Three e2e tests in core-flows.spec.ts cover Layer 1 in a real browser: specification creation on the NDO's own clone cell, a typed governance rule reachable through the spec link, and the open-access scope lock. The suite goes from 18 to 21 tests, 23 including multi-agent.spec.ts.

Documentation

New: Source-NDO.md, source-ndo-requirements.md, source-ndo-paper.md, source-valueflows-integration.md, complete-resource-specification.md, plus the ArtCoin application docs and user stories. Updated: requirements.md, resources.md, governance.md, ndo_prima_materia.md, implementation_plan.md, IMPLEMENTATION_STATUS.md, resource_zome.md, API_REFERENCE.md, and the valueflows-dsl.md / vf:Source design notes.

Review follow-ups

Fourteen commits on top of Tibi's six, across three review rounds. Every behavioural fix below was demonstrated red before its fix landed.

Round 1 — integrity (09224eb, 923b3d8, 74532aa)

1. Layer 0 classification was writer-controlled (09224eb)

validate_create_governance_rule built its ResourceClassification from rule.property_regime / resource_nature / rivalry_override, all supplied by the caller, and never checked them against the referenced ndo_identity_hash. A TransferCondition{Ownership} rule on a Nondominium NDO passed validation simply by declaring property_regime: Private. The capture-resistance guarantee (REQ-RES-03) was self-declared.

Integrity now reads the referenced NondominiumIdentity and rejects any mismatch before evaluating constraints. All three fields are immutable on Layer 0, so the genesis record reached through the stable hash is authoritative. validate_update_governance_rule delegates to create, so updates are covered.

2. The Layer 1 lifecycle gate read the wrong stage (923b3d8)

validate_create_resource_spec called must_get_valid_record(ndo_identity_hash), which returns the genesis record. lifecycle_stage mutates through the update chain, so the gate judged every activation against the creation-time stage: an NDO created at Ideation and advanced to Specification could never grow a spec (the primary intended flow), while one created at Active and since Deprecated still could.

ResourceSpecification now carries ndo_state_hash. Integrity cannot walk an update chain forward (the set of updates grows, so validation would not replay), but backward is deterministic: every Update names exactly one predecessor via original_action_address, and that edge never changes. resolve_ndo_state reads the observed entry, walks back to the genesis Create, and proves the root equals ndo_identity_hash before gating on the stage. Capped at 64 hops. The coordinator derives the field via resolve_latest_ndo_record, so honest clients are correct by construction, and immutability on update prevents re-pointing a spec at a newer state to launder a rejected activation.

3. Test hardening (09224eb, 923b3d8)

Six new Sweettests in dnas/nondominium/tests/src/resource/mod.rs: governance_rule_rejects_classification_drift_from_layer0, ..._nature_drift_..., nondominium_ownership_transfer_not_bypassable_by_misdeclared_regime, governance_rule_accepts_classification_matching_layer0 (over-tightness guard), resource_spec_allowed_after_advancing_out_of_ideation, resource_spec_rejected_after_deprecation. Classification tests were 3 failed / 2 passed pre-fix and 9 passed post-fix; lifecycle tests 0 passed / 2 failed pre-fix and 11 passed post-fix.

The pre-existing check_rule_data_constraints_blocks_nondominium_ownership_transfer looked like it covered bug 1 but did not: it exercises the pure dry-run query, which takes classification as a parameter.

4. Red e2e pipeline (74532aa)

Pre-existing on dev (run 31285564294), not a Layer 1 regression. The Phase 0 clone-signing guard in core-flows.spec.ts created a group clone cell and never removed it; the UI enumerates group clone cells off appInfo, so the leftover cell rendered as a real group, hasGroups became true, and the onboarding CTA never mounted. Fixed with disableCloneCell + admin.deleteCloneCell in a finally block. expectEmptyLobby now asserts the precondition explicitly so a future leak names itself instead of surfacing as "element not found". e2e went from 10 passed / 1 failed / 7 skipped to 18 passed / 0 failed.

Round 2 — the feature was broken end to end while CI was green (b898f41, 2e3129a, d7b7197, f57ef43, b3bf2ae, ca3f6d6)

Verified in a real browser against the live two-agent network, which is how all five of these were found. CI was 7/7 green throughout.

5. Layer 1 and Layer 2 called the wrong cell (b898f41). PR #128 moved every NDO into its own cloned ndo cell. The whole Layer 1 and Layer 2 UI still called zome_resource / zome_gouvernance on the shared provisioned nondominium cell, where the NDO identity was never written, and failed with Linked NondominiumIdentity not found. NdoService.resolveCellIdForNdo now resolves the NDO's clone from its anchor coordinates (provisioning it for a peer who never joined) and an optional cellId threads through to every NDO tab and modal. The same commit routes GovernanceTab's second, unrouted spec fetch, which was writing rules with no specification_hash: committed fine, unreachable by every read path, a silent write-only rule.

6. Layer 2 was write-only (2e3129a). propose_commitment returned ok and get_all_commitments returned zero in the same cell and session. Cause: record.entry().to_app_option::<EntryTypes>() deserializes into the enum wrapper rather than the inner struct, so the match arm never fired, every record was skipped, and the read returned zero with no error anywhere. Ten sites across commitment.rs, validation.rs, economic_event.rs and ppr.rs; zero EntryTypes readers remain under dnas/ or crates/. An earlier hypothesis (an un-ensure()d anchor path) was falsified by experiment: fixing one read left a second untouched test red.

7. get_ndo_transition_history did not exist (d7b7197). The UI called it on every NDO open; the function was absent from the entire Rust tree and the service swallowed the error, so the panel read "0 transitions" forever and REQ-UI-NDO-04 was documented as implemented. Implemented over the Layer 0 update chain, and a failed read now renders "unavailable" rather than disguising itself as empty state.

8. Open regimes imply Public scope (f57ef43, REQ-RES-03). A spec on a Nondominium NDO was accepted with scope: Project, which omits it from the global discovery anchor: the resource stays unownable while becoming invisible to everyone outside the narrowing group. That is enclosure by visibility. check_scope_coherence is a Hard predicate enforced on create and update (a create-only gate is laundered by one edit), and the form no longer offers the invalid choice. Negatives and positives both tested: Public on Nondominium and Project on Commons still pass, so it is a gate, not a blanket refusal.

9. Transition history rendered raw byte arrays (b3bf2ae). The panel showed By 132,32,36,253,… where the rest of the app shows uhCAk…, and the copy affordance put the byte string on the clipboard. Root cause was a type declaring agent: string when it is Uint8Array at runtime, which is why .slice typechecked and bun run check stayed green over the defect.

Round 3 — e2e coverage for the headline feature (39accc7)

Rounds 1 and 2 make the same point twice: a green suite closed a test criterion and nothing else. Sweettest creates the NDO and the spec in the same cell, so it cannot observe a cell-routing bug, and no e2e touched Layer 1 at all. Three tests now do, each written so the load-bearing claim is one only a two-cell browser test can make:

  • a specification created through the UI lands on the NDO's own clone cell and not on the shared one. get_specifications_for_ndo traverses NdoToSpecification, so a non-empty result is the Layer 1 activation edge itself (REQ-NDO-L1-01).
  • a typed AccessRequirement rule is reachable through the spec's SpecificationToGovernanceRule link, the only assertion that distinguishes an attached rule from an orphan. Asserts the RuleData discriminant, so a stringly-typed regression surfaces here.
  • an open-access regime fixes the scope select to Public and disables it, and the value that reaches the DHT is Public. The DOM assertion is the affordance; the read-back is the claim, because the UI lock derives from the cached anchor regime and fails open on a cold cache.

Each was demonstrated red by reverting the behaviour it guards, one at a time. 23/23 across both spec files, +7s of test time.

Decisions

Option Rejected because
Keep rule_data as a JSON string No schema enforcement, no tooling, and validation could not reason about rule semantics. The typed enum is the GovernanceRuleType migration called for in ndo_prima_materia.md.
Store GovernanceRuleType alongside the payload Two sources of truth that can disagree. Deriving it from the RuleData discriminant makes the mismatch unrepresentable.
Extend ResourceState with more variants It conflated two orthogonal axes. LifecycleStage (Layer 0, maturity) and OperationalState (instance, current process) now move independently, per REQ-NDO-OS-06.
Block every constraint violation in the integrity zome Some coherence rules are advisory, not invariants. Hard blocks at integrity; Soft surfaces as coordinator and UI advice.
Make rivalry a required stored field Nature implies it in nearly every case. It is derived from ResourceNature::default_rivalry() with an immutable rivalry_override for the exceptions (for example a rivalrous Service slot).
Read Layer 0 during rule validation (reversed in review) The original rationale was that integrity validation cannot afford DHT reads. It can: validate_create_resource_spec already did one sixty lines up. Denormalization is kept for the constraint evaluation, but integrity now reads Layer 0 and rejects any field that disagrees, so the declared classification is no longer trusted.
Move the lifecycle gate to the coordinator Cheaper than the backward chain walk, but a coordinator is not a trust boundary: any client can write to the DHT directly.
Bind EconomicEvent / Commitment to their NDO through the resource Not possible without a data-model change. validate_action_against_ndo judges the action against whatever ndo_identity_hash the writer supplies, and nothing binds it to resource_inventoried_as; EconomicResource carries no spec or NDO field, and integrity cannot follow a link deterministically. Accepted limitation: the Layer 2 capture-resistance gate is writer-controlled in the same way Layer 1's was before 09224eb. Closing it means giving EconomicResource an NDO pointer, which is its own PR.
Validate governance entries on the update path in this PR Deferred, and stated plainly because the gate this PR adds is a create-path gate. zome_gouvernance's OpEntry::UpdateEntry arm guards NdoHardLink and Agreement and falls through for the other seven entry types, so a create-validated event can be updated past the predicate, and a ValidationReceipt.approved can be flipped after the fact. This is pre-existing and byte-identical on dev, not introduced here, and the fix splits by fact versus plan (events, receipts and claims immutable; commitments re-validated with frozen identifying fields), which is five or more red-then-green pairs on a suite whose resource shard already runs 19 minutes. Follow-up issue.
Resolve the latest NDO stage inside integrity Impossible, not merely expensive. Forward chain resolution is non-deterministic under replay because the set of updates grows after the fact. Hence ndo_state_hash. Accepted limitation: an author writing directly to the DHT can present an old-but-eligible state.

Breaking changes

This changes DHT entry shapes and link types, so it is a DNA-hash-breaking change. No migration path is provided; existing test networks need to be recreated.

  • EconomicResource.state: ResourceState becomes operational_state: OperationalState. ResourceState is removed.
  • GovernanceRule.rule_type / rule_data strings become a single typed RuleData, plus three required classification fields, which integrity now checks against Layer 0.
  • ResourceSpecification gains required ndo_identity_hash, ndo_state_hash, and scope.
  • LinkTypes::ResourcesByState becomes ResourcesByOperationalState; NdoToSpecification is added.

How to test

nix develop
bun install
bun run build:happ

# Sweettest: 2 threads. Six threads SIGTERMs the process mid-run with 11
# conductor-spawning tests in flight, and it reads exactly like a test failure.
CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest --test resource -- --test-threads 2
CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest --test governance -- --test-threads 2
CARGO_TARGET_DIR=target/native-tests cargo test -p nondominium_sweettest --test nondominium -- --test-threads 2

bun run e2e     # Playwright against real conductors: 23 tests, ~4 min

bun run start   # 2-agent network: create a spec from an NDO, author a typed rule, move operational state

Verified: all five Sweettest targets green locally at --test-threads 2; the full e2e suite green locally at 23/23 across both spec files. CI 7/7 green on 39accc7 (run 33276486740): build, five Sweettest shards, and e2e at 23 passed with no retries. The three new Layer 1 tests were each confirmed present and passing in the CI e2e log rather than inferred from the job's status.

Documentation

Also in the diff, from Tibi's own commits and listed here so nothing in the changed-file list is undescribed: pai/human_ai_collaboration.md (a research note on AI cognition and cognitive offloading), pai/cursor-rules/10-domain-enums.md, root Artcoin.md, documentation/Applications/ (ArtCoin and Healthnet application docs, including a 2 MB PDF), the ArtCoin user stories, and documentation/hREA/valueflows-1.0-compliance.md.

The deprecated Tryorama suite under tests/ was updated to the new entry shapes so it still compiles; it remains deprecated per tests/DEPRECATED.md and gates nothing.

Extensive. See the Documentation subsection under Changes for the full list of added and updated files.

Related

Related issues:

Follow-ups filed rather than fixed here, so the deferrals are visible:

Deferred, deliberately out of scope here:

  • Layer 2 activation (NdoToProcess), and wiring the constraint predicates into evaluate_transition. zome_gouvernance/src/transition.rs:7 carries a live TODO(§5 item 2) on whether existing write paths should funnel through evaluate_state_transition.
  • Design collision to settle before Source-NDO lands: check_capture_resistance Hard-blocks Consume on Nondominium, but the Source-NDO requirements this same PR documents mandate recording extraction from Nondominium / CommonPool sources (REQ-PROC-10, REQ-SOURCE-EVENT-01). One of the two has to give.

Closes #136

TiberiusB and others added 10 commits August 6, 2026 15:14
Introduced a TypeScript service layer for Group DNA, replacing the GroupService stub with a callZome implementation. Updated group types and services to support new API functionalities. Enhanced documentation to include API references, architecture overviews, and test commands for Group DNA, ensuring clarity on the new structure and interactions within the system.
…governance framework

Added comprehensive documentation for the Source-NDO, a new ontological primitive representing generative ecological systems. This update includes the `source-ndo-requirements.md` detailing its role, governance patterns, and integration within the Nondominium architecture. Enhanced existing documents to reflect the necessity of the `vf:Source` ValueFlows extension and the adaptive governance loop for ecological systems. This change aims to clarify the framework for managing ecological commons and ensure accurate representation of environmental interactions within the economic information system.
Introduced a new document detailing the integration of the `vf:Source` primitive into the Nondominium's Valueflows model. This comprehensive design outlines the purpose, structure, and implications of the `vf:Source` for generative ecological systems, distinguishing it from existing primitives. Additionally, updated the Valueflows DSL documentation to clarify the distinction between the current and planned capabilities, emphasizing the future integration of the `vf:Source` within the Nondominium architecture.
…management

Added the OperationalState enum to represent the current process condition of EconomicResource instances, allowing for states such as Available, Reserved, InTransit, InStorage, InMaintenance, InUse, and PendingValidation. Implemented functionality for creating, updating, and querying resources by their operational state. Updated relevant tests and documentation to reflect these changes, enhancing the resource management capabilities within the Nondominium architecture.
…nd governance integration

Revised the documentation for the Artcoin application within the Nondominium framework. Key updates include detailed descriptions of user stories for art circulation, distribution, and production, emphasizing the roles of individual patrons and venues. Enhanced governance rules and operational states for artworks are now clearly outlined, alongside the integration of Private Participation Receipts (PPRs) for tracking reputation and custody. This update aims to provide a comprehensive understanding of the Artcoin ecosystem and its alignment with Nondominium's resource-sharing capabilities.
…ules

Added new modules for resource classification and governance rules within the Nondominium framework. The `constraints` module includes predicates for evaluating resource classifications, handling constraint violations, and ensuring compliance with governance rules. The `rule_data` module defines various governance rule types, including access requirements and transfer conditions. This update enhances the integrity validation process and supports the management of resources in a decentralized environment, aligning with the overarching goals of the ArtCoin project.
…mpty

The Phase 0 signing guard creates a `group` clone cell on agent 1 and never
removes it. The UI enumerates group clone cells straight off `appInfo`, so the
leftover cell rendered as a real group in the sidebar, `hasGroups` became true,
and the next test's create-or-join onboarding CTA never mounted.

Disable and delete the clone in a `finally` block, and expose `appId` on
SeedClient for the admin-scoped delete.

Pre-existing failure on `dev` (run 31285564294), not a Layer 1 regression.
The Layer 1 constraint predicates were only ever exercised through
`check_rule_data_constraints`, which takes the classification as a parameter.
Nothing bound a rule's denormalized `property_regime` / `resource_nature` /
`rivalry_override` to the NDO it claims to describe, so capture resistance was
self-declared: an ownership-transfer rule on a Nondominium NDO passed simply by
writing `Private` on the rule entry.

Integrity now reads the referenced NondominiumIdentity and rejects any
mismatch before evaluating constraints. All three fields are immutable on
Layer 0, so the genesis record reached through the stable hash is authoritative
and no update-chain walk is needed — the same read `validate_create_resource_spec`
already performs.

Four Sweettests cover the boundary: regime drift, nature drift, the
misdeclared-regime bypass of REQ-RES-03, and a matching-classification guard so
the binding cannot be over-tight. Verified red against the unfixed zome
(3 failed / 2 passed) before the fix, green after (9/9).

CI gains a `sweettest` job running the shared-crate unit tests and all five
Sweettest targets, with `e2e` now gated behind it as the workflow comment asked.
`--test-threads 2`: 6 threads with 11 conductor tests in flight gets SIGTERM'd.

The empty-lobby e2e test asserts its precondition via `expectEmptyLobby` so a
leaked group names itself instead of surfacing as "element not found".
…st in CI

The Layer 1 lifecycle gate read `must_get_valid_record(ndo_identity_hash)`,
which returns the *genesis* record. `lifecycle_stage` mutates through the
update chain, so the gate judged every activation against the stage the NDO was
created at: an NDO created at Ideation and advanced to Specification could never
grow a spec, while one created at Active and since Deprecated still could.

`ResourceSpecification` now carries `ndo_state_hash` — the NDO action the author
observed — alongside the stable `ndo_identity_hash`. Integrity cannot walk an
update chain forward (the set of updates grows, so validation would not replay),
but backward is deterministic: every Update names exactly one predecessor via
`original_action_address` and that edge never changes. `resolve_ndo_state`
therefore reads the observed entry, walks back to the genesis Create, and proves
the root matches `ndo_identity_hash` before gating on the stage. Capped at 64
hops; the chain is at most 10 stages in practice.

The coordinator derives the field via `resolve_latest_ndo_record`, so honest
clients are correct by construction, and `ndo_state_hash` is immutable on update
so an edit cannot re-point a spec at a newer state to launder an activation the
create-time gate rejected. Accepted limitation: an author writing directly to
the DHT can present an old-but-eligible state.

Two Sweettests cover both directions, verified red against the unfixed zome
(0 passed / 2 failed) and green after (11/11).

CI: the single sweettest job measured 61 min (run 31736241424) — 16 min compile
plus 40 min of tests back-to-back. Sharded one job per target with a shared
rust-cache key, and dropped the custom CARGO_TARGET_DIR: `target/native-tests`
is right locally (keeps native artifacts away from the wasm build) but falls
outside what rust-cache saves, so every run recompiled holochain test_utils.
@Soushi888

Copy link
Copy Markdown
Collaborator Author

Review: REQUEST CHANGES

Reviewed at 923b3d8 against origin/dev. The Layer 1 architecture is sound and the two integrity bugs caught in review were fixed the right way. But the same writer-controlled-classification shape survives on the Layer 2 path, and the CI job this PR builds to gate merges leaves one suite out.

Merge state mergeable, not behind dev
CI build pass; 5 sweettest shards pending; e2e gated behind them
Description 3 areas in the diff not in ## Changes
Docs gate 2 mapped files missed, 1 stale section
Tests gate 6 negative tests, red-before-green demonstrated

Request changes

R1. Layer 2 capture resistance is still self-declared.

EconomicEvent and Commitment each carry their own ndo_identity_hash, and nothing binds it to resource_inventoried_as. validate_action_against_ndo therefore judges the action against whatever NDO the writer names, so Transfer on a Nondominium resource passes by naming a Private NDO instead.

The new test demonstrates the gap rather than closing it: nondominium_transfer_event_is_hard_rejected (dnas/nondominium/tests/src/governance/mod.rs:348) passes a stub resource_inventoried_as of ActionHash::from_raw_36(vec![7u8; 36]), an action hash with no relationship to the NDO whose regime is being enforced.

This is review follow-up #1 relocated, not fixed. Instead of declaring a false regime inline, the writer declares a false NDO pointer. REQ-RES-03 remains self-declared on the Layer 2 write path.

Two ways out:

  • Give EconomicResource a spec pointer (conforms_to) so integrity can resolve resource to spec to NDO deterministically, and check event.ndo_identity_hash against it.
  • Or record the limitation in the Decisions table the way ndo_state_hash was. The PR is admirably explicit about the state-hash limitation; this one is not mentioned at all.

R2. GovernanceRule.ndo_identity_hash is mutable on update.

ResourceSpecification gets explicit immutability checks for both ndo_identity_hash and ndo_state_hash in the UpdateEntry arm (dnas/nondominium/zomes/integrity/zome_resource/src/lib.rs:217 and :225). GovernanceRule gets none: validate_update_governance_rule delegates straight to create, which validates the classification against whatever new pointer the update names.

So a rule created under a Nondominium NDO can be updated to point at a Private NDO, with property_regime changed to match, and its rule_data swapped to TransferCondition{ Ownership }. Mirror the spec's immutability check.

R3. group_sweettest still gates nothing.

The new matrix runs cargo test --package nondominium_sweettest --test ${{ matrix.target }} over [misc, person, governance, resource, nondominium]. dnas/group/tests/Cargo.toml defines two more targets, group and ndo_anchor, and this PR adds ndo_anchor_round_trip_public_regime to the latter. Given the PR's stated point ("the primary backend suite per CLAUDE.md had never gated a merge"), the Group DNA suite should join the matrix.

R4. scope changes leak the global discovery anchor.

create_resource_specification skips the AllResourceSpecifications anchor for ResourceScope::Project. scope is mutable and update_resource_specification carries the new value through without reconciling the anchor. So Project widened to Public stays permanently invisible to get_all_resource_specifications, and Public narrowed to Project stays visible.

R5. Documentation mapping (REVIEW.md §6).

  • documentation/zomes/governance_zome.md documents LogEconomicEventInput (line 284) and ProposeCommitmentInput (line 343). Both gained a required ndo_identity_hash field, and two new externs landed in the same zome (evaluate_state_transition, check_action_constraints). The file is untouched in this diff.
  • documentation/TEST_COMMANDS.md is untouched although -- --test-threads 2 is now load-bearing (6 threads SIGTERMs the run) and the CI invocation changed shape.

R6. IMPLEMENTATION_STATUS.md is stale on the thing this PR ships.

Line 193 still reads:

Governance-as-Operator Architecture ❌ Specified, not implemented

... the Rust DNA does not currently define GovernanceTransitionRequest, TransitionContext, GovernanceTransitionResult, evaluate_state_transition, ...

This PR adds all four (crates/shared/src/io/governance.rs, zome_gouvernance/src/transition.rs). request_resource_transition is still absent, so the section is partial rather than complete, but the ❌ header and the flat denial are both wrong now. The file was edited (+286) here, so this is a miss rather than an untouched file.

R7. Description-to-diff gaps.

Three areas are in the diff but absent from ## Changes:

  • pai/human_ai_collaboration.md (+677) — an essay on AI cognition and cognitive offloading, unrelated to NDO Layer 1.
  • Artcoin.md at repo root (+53) — duplicates the framing already in documentation/Applications/nondominium_artcoin.md.
  • documentation/Applications/Healthnet.pdf — a 2 MB binary, no LFS.

Split them into their own PR or name them.


Suggestions

  • validate_action_against_ndo reads the genesis NDO record via must_get_valid_record(ndo_identity_hash), so lifecycle_stage in its ResourceClassification is the creation-time stage. This is precisely the bug fixed for validate_create_resource_spec. No predicate reads lifecycle_stage today, so it is latent, but a future lifecycle predicate would silently inherit the wrong stage. Pass None, or leave a comment saying why not.
  • create_governance_rule takes property_regime / resource_nature / rivalry_override from the caller, while create_resource_specification derives them from Layer 0. Derive in both, so honest clients cannot hit a spurious integrity rejection they have no way to diagnose.
  • get_resource_specification_with_rules fetches rules via get(original_hash) on the link target, so update_governance_rule results never surface. Relatedly, update_resource_specification links new rules to updated_spec_hash, which that reader is never called with.
  • dnas/group/tests/src/ndo_anchor/mod.rs mirrors rivalry_override as Option<String> where the real field is Option<Rivalry>. Only None is used today so it passes, and it will misencode silently the moment anyone sets it.
  • Worth a second thought on committing a 2 MB PDF to git without LFS.

Notes

The check_capture_resistance vs Source-NDO Consume collision flagged in the PR description is real and correctly deferred. R1 sharpens it: once extraction from a Nondominium or CommonPool source is legitimate (REQ-PROC-10, REQ-SOURCE-EVENT-01), the unbound ndo_identity_hash becomes the only thing standing between "recorded extraction" and "laundered transfer". Worth settling R1 before Source-NDO lands, not after.

Everything else holds up well:

  • resolve_ndo_state's backward chain walk is the right call, and the Decisions table is honest about what it cannot do rather than papering over it.
  • The CI sharding rationale (why no custom CARGO_TARGET_DIR, why shared-key, why --test-threads 2) is documented in the workflow itself, which is exactly where the next person will need it.
  • The four new Svelte components are clean Svelte 5 runes throughout, no $: anywhere.
  • Deriving GovernanceRuleType from the RuleData discriminant rather than storing it alongside is the right call, and the Decisions table gives the right reason.

@Soushi888

Copy link
Copy Markdown
Collaborator Author

Review verdict: APPROVE (Wave 2 — merge after #129, rebase first)

Deep review of the 98-file diff, focused on the four load-bearing surfaces.

Typed RuleData (shared crate) — clean. Tagged enum with four variants, GovernanceRuleType derived from the discriminant and never stored separately, ungated module so integrity zome, coordinator, and unit tests share one schema. UI mirrors in packages/shared-types/governance.types.ts.

Classification-coherence enforcement — this is the strongest part of the PR. validate_create_governance_rule resolves the referenced Layer 0 record with must_get_valid_record and rejects any GovernanceRule whose denormalized property_regime / resource_nature / rivalry_override diverges from it, before the constraint predicates run. Without that binding the classification is writer-controlled and every regime-driven constraint (capture resistance above all) becomes advisory — an ownership-transfer rule on a Nondominium NDO would pass by declaring Private. The Sweettest nondominium_ownership_transfer_not_bypassable_by_misdeclared_regime asserts exactly this attack is closed. must_get_valid_record inside validation also gets Holochain's dependency-wait semantics for free: a rule whose NDO record has not gossiped yet is held, not rejected.

Layer 1 activation gatevalidate_create_resource_spec gates on the stage read through ndo_state_hash (the observed state, update-chain walked) rather than the genesis record, with the right reasoning recorded: genesis always carries the creation-time stage, so gating on it would reject the ordinary "create at Ideation, advance, then activate" flow while accepting a since-Deprecated NDO. The state hash is also checked to belong to the claimed NDO. The UpdateEntry arm enforces immutability of both hashes, including the subtle one: an edit re-pointing ndo_state_hash at a newer eligible state would launder an activation the create-time gate rejected — that path is closed too, with coverage (resource_spec_rejected_at_ideation_stage, ..._allowed_after_advancing_out_of_ideation, ..._rejected_after_deprecation).

OperationalState splitEconomicResource.state: ResourceState becomes operational_state: OperationalState. This is a breaking entry-type change: old serialized entries will not decode as the new struct. Acceptable now (pre-production, no released pilot data, and #130 changes every DNA hash anyway), but it belongs in the release notes of whatever ships first after this lands. The lifecycle test economic_resource_operational_state_lifecycle covers the transition semantics.

One design question, non-blocking: gated_access_contradicts_permissionless_regime fires as Soft even under the Nondominium regime, so a Gated access rule on a Nondominium NDO warns but commits. REQ-RES-01 says permissionless access under defined governance rules, so a discretionary gate is at least in tension with the regime's spirit. Soft-severity here reads as a deliberate configurability choice (Hard would make the constraint unreachable for communities that want it) — worth one sentence in the ADR recording that it is intentional, so a future reader does not "fix" it to Hard.

Severity table for the record — ownership-transfer under uncapturable regime: Hard; under non-uncapturable non-transfer regimes: Soft; gated access on Nondominium: Soft; transport on non-physical nature: from check_transport_applicability. Consistent with the constraints module docs.

Merge mechanics: rebase onto post-#129 dev (base will have moved by two merges + the #137 CI gate), full matrix re-run expected green, then merge.

…fix)

Five conflicts, all resolved by keeping both sides rather than picking one:

- crates/shared/types.rs: #132's Rivalry/ResourceScope/OperationalState and
  dev's NdoDnaProperties are independent additions; both kept.
- zome_resource integrity: kept #132's regime-semantics hook AND dev's ADR-013
  binding check. Dropping the latter would silently remove the guarantee that a
  GovernanceRule's classification cannot diverge from Layer 0.
- ndo_anchor tests: took dev's shared-crate import (#128 deliberately replaced
  the hand-kept mirror, which had already drifted on `initiator`), then
  re-applied #132's additions on top: rivalry_override on the NdoInput/NdoEntry
  mirrors and the Public-regime anchor test. That test was written against the
  old mirror, so it needed adapting: NdoDnaProperties has no `initiator` field,
  and anchor_input_from now takes the initiator as a separate argument.
- ndo.service.ts: took dev's side. #132's mapListingToDescriptor is dead under
  the anchor model (zero callers) and its identityToDescriptor was a duplicate
  definition. rivalry_override survives in the retained field mapper.
- IMPLEMENTATION_STATUS.md: neither side was accurate. Arbitrated against the
  code: 20 externs, and no get_all_groups.
@Soushi888

Copy link
Copy Markdown
Collaborator Author

Rebased onto dev (now carrying #137, #128, #138)

Merged rather than rebased: this branch is 10 commits and the conflicts repeat across most of them, so a merge resolves each one once and the squash-merge flattens it anyway. Five conflicts, all resolved by keeping both sides rather than picking a winner. Full reasoning is in the merge commit message; the two that matter for review:

The ADR-013 binding was at risk. validate_create_nondominium_identity had a conflict between this branch's regime-semantics hook and dev's ADR-013 check (entry classification must match the NDO cell's DNA properties). Taking either side alone would have compiled fine. Taking dev's alone would have dropped the Phase A hook; taking this branch's alone would have silently removed #128's classification binding — the guarantee that an NDO's entry cannot diverge from the DnaHash it was cloned under. Both are kept.

The ndo_anchor test mirror had already drifted. #128 deliberately replaced the hand-kept local enum mirror in that test with the shared-crate definition, precisely because the mirror had drifted once on initiator. This branch still carried the old mirror plus its own additions. Resolution: take dev's shared import, then re-apply this branch's additions on top (rivalry_override on the NdoInput/NdoEntry mirrors, and ndo_anchor_round_trip_public_regime). That test then failed to compile against the current types — NdoDnaProperties has no initiator field, and anchor_input_from now takes the initiator as a separate argument — and was adapted. Caught by a local cargo check, before CI.

One doc claim was wrong on both sides. IMPLEMENTATION_STATUS.md conflicted on the group coordinator API: this branch said 16 externs with no get_all_groups; dev said a different list that included get_all_groups. Neither matches the code. Arbitrated against source: 20 externs, and get_all_groups does not exist. Corrected to that.

Note on the seven-variant PropertyRegime

This branch inserts Public before Nondominium. That ordering would matter if clone properties were index-encoded, since it would shift Nondominium and change the DnaHash of every existing Nondominium NDO cell. They are not: createNdoCloneCell sends a plain JS object, holochain 0.6.0 transports it as YamlProperties, and serde encodes these unit variants by name. So existing cells are unaffected. The empirical check is that same_coordinates_derive_same_dna_hash and create_ndo_rejected_when_name_diverges_from_properties still pass on this merge; the CI run on this push is that check.

Local verification before push

  • WASM zomes: cargo check --release --target wasm32-unknown-unknown clean (warnings pre-existing).
  • cargo check --package group_sweettest --tests clean after the test fix above.

Full matrix (5 Sweettest shards + e2e) is running on this push.

Layer 1 activation puts the word "Specification" on the NDO detail page in
more than one place: the identity panel's lifecycle stage, the Layer 1
specification panel, and its create modal. The multi-agent live-read test
asserted `getByText('Specification', { exact: true })`, which became a
Playwright strict-mode violation the moment Layer 1 rendered.

Add `data-testid="ndo-lifecycle-stage"` to the stage field and assert on
that. A structural selector would have worked too, but it would break again
the next time the panel is restyled; the test id says what the test means.

Local e2e: 19 passed, including the previously failing case.
PR #128 moved every NDO into its own cloned `ndo` cell, but the Layer 1 and
Layer 2 UI kept calling `zome_resource` and `zome_gouvernance` on the shared
provisioned `nondominium` cell, where the NDO identity was never written.
Creating a resource specification therefore failed with
`Guest("Entry operation failed: Linked NondominiumIdentity not found")` from
`resource_specification.rs:53`, and every NDO-scoped read returned nothing.

`NdoService.resolveCellIdForNdo` resolves the NDO's clone cell from its anchor
coordinates, provisioning it for a peer who never joined, and returns null for
legacy NDOs still living in the shared cell so those keep working. `NdoView`
resolves it once per NDO and passes `ndoCellId` to the resources, governance
and activity tabs and to the four modals below them. An optional `cellId`
threads through `zome-helpers` into both zome services and both stores; every
call that does not pass one keeps its previous role-name routing.

Two further defects surfaced while verifying this in the browser:

- The governance tab's "+ New rule" handler carried a second, unrouted
  `fetchSpecificationsForNdo`. It returned nothing, so the rule was written
  with no `specification_hash` and no read path could surface it again. Routed,
  and guarded so the editor refuses to open when the NDO has no Layer 1 spec.

- `anchorToDescriptor` omitted `rivalry_override`, leaving `bun run check` red
  on the branch. The anchor caches only the card fields, so it is null there
  and the live read on open supplies the real value.

Verified in real Chrome against the 2-agent dev network: a specification and a
typed AccessRequirement rule both create and read back. A probe through the
app's own client confirms placement, `get_specifications_for_ndo` returning
0 for the shared cell, 0 for the provisioned `ndo` cell and 1 for the NDO's
own clone. `bun run check` reports 0 errors and 0 warnings.
Resolves both conflicts by keeping #129's membership additions and the
round 1 cell-routing fix together, since neither replaces the other:

- NdoView.svelte: the `membersStubMessage` placeholder is gone (membership
  is real now, so `membersError` carries the failure), and `ndoCellId`
  stays, because every Layer 1 and Layer 2 tab still needs the NDO's own
  clone cell.
- ndo.service.ts: `resolveCellIdForNdo` keeps its interface entry and its
  implementation alongside the real `joinNdo` / `getNdoMembers`, which now
  call `join_ndo`, `is_ndo_member`, and `get_ndo_members` on the ndo cell.
Every Layer 2 read path deserialized the fetched record with
`to_app_option::<EntryTypes>()`. A stored entry is the bare inner struct,
not an externally tagged enum, so that deserialization can never succeed.
Each site then pattern-matched the result inside `if let Ok(Some(..))`,
which discards both the `Err` and the `None`, so the failure surfaced as
an empty vector rather than an error. `propose_commitment` returned ok and
`get_all_commitments` returned 0 for the same commitment, in the same cell,
in the same session: Layer 2 was write-only and said nothing about it.

Read each record as the concrete entry type it holds. Nine sites, all in
zome_gouvernance, all the same one-line correction:

- commitment.rs: get_all_commitments, claim_commitment, get_all_claims,
  get_claims_for_commitment
- economic_event.rs: get_all_economic_events, get_events_for_resource
- validation.rs: get_validation_history, get_all_validation_receipts,
  check_validation_status
- ppr.rs: extract_private_participation_claim

claim_commitment is worth calling out: it took the same failure through a
`match` and returned "Invalid commitment entry", so it could never claim
any commitment at all.

The anchor paths are not at fault. `Path::from("all_commitments")` is never
`.ensure()`d, but neither is `Path::from("ndo_identities")`, and NDO
discovery has always worked. Confirmed by experiment: fixing only
get_all_commitments turned that test green while the untouched
economic-event test stayed red, and inserting `await_consistency_20_s`
before the read changed nothing.

Tests: two Sweettests asserting an author can read back what it just wrote,
red on the previous head (0 != 1) and green now.
The UI called `zome_resource.get_ndo_transition_history` on every NDO open.
The function did not exist anywhere in the Rust tree, the console logged
"Attempted to call a zome function that doesn't exist", and the service
caught it into `[]`, so the panel read "Lifecycle history - 0 transitions"
forever. REQ-UI-NDO-04 was documented as implemented and was not.

Implement it over the NondominiumIdentity update chain. `update_lifecycle_stage`
is the only writer of updates, and integrity validation rejects any update
that touches a field other than lifecycle_stage, successor_ndo_hash, or
hibernation_origin, so the chain IS the history: no separate log entry to
write, and none that can drift from the entry it describes. Each step
reports the stage it came from, the stage it went to, and the author and
timestamp of the Update that recorded it. Traversal follows the most recent
update at each step, the same rule resolve_latest_ndo_record uses, so the
history can never disagree with the stage get_ndo displays.

event_hash is the ActionHash of the Update action, which always exists and
identifies the transition precisely. It is deliberately not the triggering
EconomicEvent: transition_event_hash is optional, is null throughout the
MVP, and its NdoToTransitionEvent links hang off the NDO's original action
hash with nothing tying a given link to a given step, so attributing one to
a specific transition would be a guess. When REQ-NDO-LC-03 makes event
generation automatic, that hash gets its own field.

UI: stop catching the call into an empty array, and distinguish "no
transitions yet" from "the read failed" in the panel. Collapsing those two
into one message is what let a missing zome function look like ordinary
empty state.

Tests: three Sweettests covering an untransitioned NDO, a three-step
forward chain, and hibernate-then-resume. All three failed on the previous
head with ZomeFnNotExists and pass now.
A Nondominium NDO is uncapturable by design and a Public one is open access
by the stewarding body's policy, but a Layer 1 spec scoped to Project or
Network is omitted from the global discovery anchor. The resource stays
unownable while becoming invisible to everyone outside the narrowing group.
That is enclosure by visibility, which is the outcome REQ-RES-03 exists to
prevent, so the predicate is Hard rather than advisory.

The predicate already had no call site anywhere outside constraints.rs, and
SpecificationCreateModal defaults scope to Project, so the invalid value was
what the default path produced. A red run against the ungated tree committed
ResourceSpecification { scope: "Project" } on a Nondominium NDO and returned
a real spec_hash.

Enforced on update as well as create: scope is mutable, so a create-only gate
is laundered by one edit. The UpdateEntry arm already freezes ndo_identity_hash
and ndo_state_hash, so the NDO reached on update is the one the create gate
judged.

The UI locks rather than warns: the scope select is fixed to Public and
disabled on an open-access NDO, with the reason shown in place of the generic
hint. propertyRegime threads from NdoView through ResourcesTab to the modal.

Tests: 4 Sweettests in the resource target (two negative, two positive) and
5 unit tests on the predicate. Red 13 passed / 2 failed, green 15 passed / 0
failed. Both positives pass in both runs: this gates enclosure without
blocking Layer 1 activation or touching regimes where scope and regime are
legitimately orthogonal.
The lifecycle history panel rendered `agent` and `event_hash` as
comma-separated decimal bytes (`132,32,36,253,73,...`) because it called
`.slice(0, 10)` on values the conductor returns as Uint8Array. `.slice` on a
typed array returns a typed array, and Svelte stringifies that as a byte list.
It was the only surface in the app showing a Holochain hash that way.

The copy affordance made it more than cosmetic: the clipboard received the
byte-array string rather than an action hash, so REQ-UI-NDO-04's copy-to-
clipboard yielded something that cannot be pasted anywhere expecting a hash.

Root cause is the type, not the component. `NdoTransitionHistoryEvent`
declared `agent: string` and `event_hash: string`, which is false at runtime
and made `.slice(0, 10)` typecheck as a string slice. Corrected to AgentPubKey
and ActionHash so the compiler stops endorsing the mistake, with the reason
recorded on the fields.

Verified red then green on the type: with the corrected type and the old
component, `bun run check` reports `Argument of type 'HoloHash' is not
assignable to parameter of type 'string'` on the clipboard call. Note the
compiler catches the copy defect but not the render defect, since `.slice` on
a Uint8Array is legal; the render fix is not compiler-enforced and stays a
convention shared with ActivityTab and CommitmentCreateForm.
@Soushi888

Copy link
Copy Markdown
Collaborator Author

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

PM review, round 2: APPROVE

Reviewed against the round 2 briefs for B0 through B3 plus the findings that came out of dogfooding this branch in a real browser. Four commits landed since round 1: 2e3129a, d7b7197, f57ef43, b3bf2ae.

Re-derived

Every claim below was produced by this reviewer, not read off a report.

  • Ten to_app_option::<EntryTypes>() sites removed, zero remaining anywhere under dnas/ or crates/, counted by hand. The commit message says nine while enumerating ten functions; the prose miscounts, the code does not.
  • Layer 2 is readable. A Use commitment created through the Activity tab renders immediately with no reload. Before 2e3129a that list returned zero for a commitment written in the same cell, the same session, by the same agent.
  • REQ-UI-NDO-04 is real for the first time. Advancing Development → Prototype produced Lifecycle history · 1 transition with the from-stage, to-stage, agent and timestamp, plus the event hash and its copy affordance.
  • B3's gate is a gate, not a refusal. #spec-scope reports value: "Public", disabled: true on a Nondominium NDO, and the red run returned a real spec_hash for ResourceSpecification { scope: "Project", ndo: <Nondominium> } before the integrity hunk went in. Both positives pass: Public on Nondominium and Project on Commons.
  • Layer 1 create-and-read survived the dev merge, re-verified on a network restarted onto new WASM with fixtures rebuilt from scratch. The round 1 verification could not carry across a merge on its own.

Taken on trust, and why

B1's economic-event half. The Log event form requires a resource action hash and nothing in the app can create an EconomicResource (#141), so that criterion is unsatisfiable through the UI for reasons unrelated to this PR. Accepted on economic_event_is_discoverable_by_its_author, red at left: 0, right: 1 and green after, plus the CI governance shard on a clean checkout. Recorded as test evidence rather than observation, because the modality gap is real.

The finding behind the findings

Three defects this round lived at a boundary the Sweettests structurally cannot see, because they assert what the zome returns and stop there: the cell-routing bug, the swallowed call to a function that did not exist, and a panel rendering Uint8Array as decimal bytes. The last is the sharpest, because the shared type declared agent: string and event_hash: string for values that are Uint8Array at runtime, so the type system was actively vouching for the wrong thing and bun run check stayed green over a live defect. That is a class of test that is missing, not three patches. Worth deciding on before the next feature lands rather than after.

Two diagnoses were also wrong and were corrected by evidence rather than argument: the anchor .ensure() hypothesis, disproved by a single-line control that turned one test green while an untouched one stayed red, and this reviewer's own claim of having seen a Project-scoped specification, retracted against a screenshot and then restored by the red run's payload.

Blocking

None.

Non-blocking, deferred past merge

  1. security(governance): seven entry types accept unvalidated updates; capture resistance and peer validation are both laundered #140, seven entry types accept unvalidated updates. The EconomicEvent and Commitment half belongs to this PR, which introduced validate_action_against_ndo and wired it to create only; the other five predate it, since dev carries the identical fall-through. Not reachable through the app, which is why it is not blocking, and not a defence, which is why it is filed.
  2. ui(ndo): no path to create an EconomicResource, and the Layer 1 scope lock fails open on a stale anchor #141, no path to create an EconomicResource, and the Layer 1 scope lock fails open on a stale anchor cache. The second was disclosed by its own author unprompted.

State

MERGEABLE. CI green on f57ef43 across build and four shards with the fifth in flight; b3bf2ae is re-running the matrix. Merge when it is green.

@Soushi888

Copy link
Copy Markdown
Collaborator Author

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

Correction to the round 2 verdict: not mergeable yet

The APPROVE above was posted while the e2e job was still gated behind the Sweettest shards. I called it on five green shards plus build without waiting for the last job in the matrix, and that was wrong of me. e2e has since finished red on b3bf2ae.

1 failed   core-flows.spec.ts:237 › lifecycle history panel present (backend rows pending)
1 flaky    core-flows.spec.ts:159 › empty lobby shows the create-or-join onboarding CTA
2 did not run
16 passed (4.0m)

The failure is B2's own stale test

test('lifecycle history panel present (backend rows pending)', async () => {
  // TODO(backend Phase 2.3): `get_ndo_transition_history` is not implemented
  // in zome_resource on dev — assert the explicit stub state today, and
  // replace with a from→to row assertion when the backend lands.
  await expect(page.getByText('No transitions recorded.')).toBeVisible();
});

The backend landed in d7b7197, which correctly removed that copy from the panel, and the assertion was never updated. The test immediately above transitions E2E Widget to Specification and asserts it on the clone cell, so by the time this one runs the NDO does have a transition and the stub state is the wrong expectation. The fix is the one the TODO already specifies: assert the from-to row and rename the test.

Why this belongs in the coverage finding rather than beside it

This is the fourth instance of the same pattern, and the sharpest yet. A criterion written for a world where the feature was missing, left asserting its absence after the feature arrived. Every Rust gate passed while a browser test naming the exact function that was implemented went red. The other three were the cell-routing bug, the swallowed call, and the type that vouched for the wrong value.

Also worth a look, not blocking

empty lobby shows the create-or-join onboarding CTA is flaky with expected an empty lobby, found 1 group(s) in the sidebar (E2E Circle), which is leaked conductor state between tests. Two further tests did not run because the failure stopped the file, so the suite has not actually been green end to end on this head.

State

Blocking until e2e is green. Everything in the verdict above stands on its own evidence; only the merge readiness changes.

…placed

`lifecycle history panel present (backend rows pending)` asserted
`No transitions recorded.`, the copy shown when get_ndo_transition_history did
not exist. d7b7197 implemented the function and replaced that copy with "No
transitions recorded yet. This NDO is still at the stage it was created in.",
so the substring no longer occurs anywhere in the DOM and the test went red the
moment the feature it named arrived. Its own TODO asked for this replacement.

The reload is load-bearing, not defensive. TransitionHistoryPanel fetches
onMount only, and the previous test transitions an already-mounted page, so the
row is on the DHT and absent from component state. Asserting the row without
reloading fails.

Two assertions beyond the row cover F9: the row text must contain a base64
hash prefix and must not contain a decimal byte-list. Neither the compiler nor
any Sweettest can catch that regression. `.slice` on a Uint8Array is legal
TypeScript, and the zome was always returning the correct bytes; only a
rendered assertion sees it.

Verified red then green on the full file rather than in isolation, since the
suite is serial and shares one page. With the render reverted to raw
`.slice()`: 1 failed, 10 passed, failing on

  Expected pattern: /uhC/
  Received string:  "Ideation → Specification By 132,32,36,50,154,19,249,16,75,207… …"

With the fix in place: 13 passed.
@Soushi888

Copy link
Copy Markdown
Collaborator Author

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

PM review, round 2 final: APPROVE, ready to merge

ca3f6d6, MERGEABLE / CLEAN. Read at job level rather than run level, which is the distinction that caused the premature approve above:

build                    pass   2m28s
e2e                      pass   7m02s
sweettest (misc)         pass   4m48s
sweettest (person)       pass   7m11s
sweettest (governance)   pass   9m29s
sweettest (resource)     pass  18m49s
sweettest (nondominium)  pass  30m26s

e2e passing in CI covers the whole suite including multi-agent.spec.ts, not just the file that was fixed, so the two tests that "did not run" behind the earlier failure are now accounted for. And because e2e was skipped rather than failed at ec1240e, d7b7197 was its first execution on this branch and there is no older failure underneath this one. The merge is clear.

What closed since the correction

ca3f6d6 replaces an assertion that outlived the absence it was written for. The old test asserted 'No transitions recorded.' under a TODO saying to replace it when the backend landed; the backend landed in d7b7197 and correctly changed that copy, so the assertion became a substring miss that would have failed even on an NDO with no transitions. The replacement asserts the row, reloads first because TransitionHistoryPanel fetches onMount only and the preceding test transitions an already-mounted page, and carries a comment saying the reload is load-bearing so nobody removes it as redundant later.

It also adds two assertions that were not asked for: the row text must match uhC and must not match a byte-list pattern. That makes the F9 render defect a regression test at the only layer that can catch it, since .slice on a Uint8Array is legal TypeScript and the zome always returned correct bytes.

The coverage finding, restated for the record

Five defects this round lived where the existing tests structurally cannot look. Four were green results proving less than they appeared to: a passing suite over a cell-routing bug, a swallowed call to a function that did not exist, a passing typecheck over a shared type that declared string for a Uint8Array, and passing shards while e2e was silently skipped. The fifth was a red result proving less than it appeared to, when a filtered test run went red before reaching the assertion under test. That one is harder to notice, because a failure feels like information and nobody re-reads it to check it failed for the right reason.

The fix is a class of test rather than five patches, and ca3f6d6 contains the smallest working instance of it.

Deferred past merge

All three are on the board in Backlog. Merge is @Soushi888's call.

Layer 1 shipped with 7/7 green CI and was broken end to end in a browser:
every Layer 1 and Layer 2 call went to the shared `nondominium` cell while
the NDO identity lived on its own clone (b898f41). Sweettest could not see
it, because it creates the NDO and the spec in the same cell, and no e2e
touched Layer 1 at all. The suite closed a test criterion and nothing else.

Three tests, each written so the load-bearing claim is one only a
two-cell browser test can make:

- a specification created through the UI lands on the NDO's own clone cell
  and NOT on the shared one. The NdoToSpecification link is what
  `get_specifications_for_ndo` traverses, so a non-empty result is the
  Layer 1 activation edge itself (REQ-NDO-L1-01).
- a typed AccessRequirement rule is reachable *through* the spec's
  SpecificationToGovernanceRule link, which is the only assertion that
  distinguishes an attached rule from the silent write-only orphan the
  unrouted read produced. Asserts the RuleData discriminant, so a
  stringly-typed regression surfaces here.
- an open-access regime fixes the scope select to Public and disables it,
  and the value that reaches the DHT is Public (REQ-RES-03). The DOM
  assertion is the affordance; the read-back is the claim, because the UI
  lock derives from the cached anchor regime and fails open on a cold cache.

Each was demonstrated red by reverting the behaviour it guards, one at a
time: routing the modal to the shared cell leaves the create dialog open
on "Linked NondominiumIdentity not found"; dropping the specification hash
writes a rule that never renders; forcing the scope lock false leaves the
select enabled. Green again with all three restored: 23/23 across both
spec files, +7s of test time.

Also returns the clone cell alongside the entry from the NDO locator, so
every Layer 1 assertion names the cell it read from rather than asserting
mere existence.
@Soushi888

Copy link
Copy Markdown
Collaborator Author

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

Review round 3: APPROVE

CI 7/7 on 39accc7 (run 33276486740), MERGEABLE / CLEAN, zero commits behind dev.

What closed since round 2

Layer 1 had no end-to-end coverage. All eighteen e2e tests passed and the eleven "Specification" matches in them were the lifecycle stage, not the Layer 1 entry. That mattered because the round 1 blocker, the whole Layer 1 and Layer 2 UI calling the shared cell instead of the NDO's clone, was found by hand, fixed by hand, and guarded by nothing. With an optional cellId now threaded through nine components, the next person who forgets it would have got a green pipeline.

39accc7 adds three tests, each demonstrated red by reverting the behaviour it guards, one at a time:

Test Reverted Failure observed
spec lands on the NDO clone cell, not the shared one modal routed to the shared cell create dialog never closes, Linked NondominiumIdentity not found
typed rule reachable through the spec link editorSpecHash dropped rule commits, never renders, unreachable by every read path
open-access regime locks scope to Public scopeLocked forced false select enabled, Project submitted

23/23 locally and in CI, first try, no retries. About 29 seconds of added e2e time.

Merging with these open, deliberately

Doc drift for the sync PR (#131)

REQ-NDO-L1-02 says Layer 1 may activate "at any lifecycle stage at or after Ideation", and the gate implemented here rejects Ideation, matching §5.2's activation table instead. One of the two has to move. §9.2 and the §3 gap list also still mark Layer 1 as post-MVP while this PR activates it.

On authorship

Tibi's six commits are intact and were checked rather than assumed: constraints.rs took +103/-1 and the one deleted line is a refactored import; PropertyRegime::Public is his and he updated resources.md §6.3 to match; the Source-NDO and ArtCoin docs are untouched. The single design decision of his that review reversed, not reading Layer 0 during rule validation, is named as reversed in the Decisions table with its reason, and his denormalization was kept and hardened rather than dropped.

@Soushi888

Copy link
Copy Markdown
Collaborator Author

SoushAI analysis. Drafted by Soushi's AI assistant, reviewed and posted by @Soushi888.

Merging now

Description refreshed against the actual diff before merge. What changed in it:

On the squash

Merging with GitHub's squash, per CONTRIBUTING.md line 90. A local strategic squash was considered and skipped: it needs a force-push, which resets all 7 green checks for another ~42 minutes, and the squash-merge collapses the grouping into a single commit on dev regardless. The strategic grouping is written into the merge commit body instead, which is the part that survives, and Tibi is credited as co-author since six of the twenty commits are his and a squash would otherwise attribute all of them to me.

Green end to end: 7/7 on 39accc7, MERGEABLE / CLEAN, zero commits behind dev. Good to merge.

@Soushi888
Soushi888 merged commit 20adb11 into dev Aug 29, 2026
7 checks passed
Soushi888 added a commit that referenced this pull request Aug 29, 2026
Issue #135 sequenced this pass behind PR #132: Layer 1 changed what these documents describe, so the harmonization pass had to land after it and then correct the sections it invalidated.

Every claim below was checked against the merged source, not against another document.

Contradictions corrected. IMPLEMENTATION_STATUS said governance-as-operator was "specified, not implemented" and named six types and functions as absent; three of the types and `evaluate_state_transition` now exist in `crates/shared/src/io/governance.rs` and `zome_gouvernance/src/transition.rs`. Only `request_resource_transition` and `evaluate_governance_transition` are still missing, which makes the evaluation path parallel and advisory rather than the mandatory funnel, and that is now what the document says. It also claimed GovernanceRule semantics are not evaluated, while `constraints.rs` enforces classification predicates Hard at integrity and Soft as advisory; the row is split into the part that is enforced and the part that is not.

ndo_prima_materia had Layer 1 marked not-started in four places. `NdoToSpecification` is in `LinkTypes` and is created on every `create_resource_specification`, so REQ-NDO-L1-01 is closed, the status matrix and §8.4 link tables are corrected, and Pattern 2 no longer says activation is unconstrained by stage. REQ-NDO-L1-02 said Layer 1 may activate at or after `Ideation`; the integrity gate rejects `Ideation` and accepts `Specification` through `Active`, matching §5.2's own activation table, so the requirement now says that.

Struct listings regenerated from source in resources.md, zomes/resource_zome.md, zomes/governance_zome.md, API_REFERENCE.md and specifications.md. `ResourceSpecification` gained `scope`, `ndo_identity_hash` and `ndo_state_hash`; `GovernanceRule` replaced the `rule_type`/`rule_data` string pair with the typed `RuleData` enum plus three denormalized classification fields; `EconomicEvent` and `Commitment` gained `ndo_identity_hash`, as did `LogEconomicEventInput` and `ProposeCommitmentInput`. `created_by` and `created_at` were documented on entries that never had them. The API reference's `ResourceSpecificationInput` and `GovernanceRuleInput` blocks described a schema with no counterpart in code at all.

The three governance design documents keep their target architecture but now open with a status banner saying which half of it exists, so a reader cannot mistake `request_resource_transition` for a callable function. The same correction lands in `pai/cursor-rules/20-architecture.md`, which is always-loaded agent context and taught the wrong call chain.

SUMMARY.md now lists every document in the tree. The Source-NDO set, the Phase B design record, ADR-010-013, the hREA release plan and two Applications pages were unreachable from the mdBook TOC.

Two items the previous commit deferred are closed by #132 rather than here: `PropertyRegime` is seven variants in both Rust and TypeScript, and `complete-resource-specification.md` is no longer empty.

Link integrity: 105 documents, 1 broken, the known em-dash anchor that resolves on GitHub.
Soushi888 added a commit that referenced this pull request Aug 30, 2026
pai/human_ai_collaboration.md arrived from #132 at the pai/ root. It reads
as harness-agnostic, so pai/shared/ is the likely home, but it is Tibi's
document and moving it as a side effect of this refactor is his call to
make, not mine.
Soushi888 added a commit that referenced this pull request Aug 30, 2026
It arrived from #132 at the pai/ root, which the new structure does not
use. The content is harness-agnostic reference material, so it classifies
as shared by the same test as everything else there: its content would not
change if we dropped support for a tool.

Move only, no edits to the document. Nothing referenced the old path except
the README note now replaced by the listing.

Authorised by @Soushi888.
Soushi888 added a commit that referenced this pull request Aug 30, 2026
It arrived from #132 at the pai/ root, which the new structure does not
use. The content is harness-agnostic reference material, so it classifies
as shared by the same test as everything else there: its content would not
change if we dropped support for a tool.

Move only, no edits to the document. Nothing referenced the old path except
the README note now replaced by the listing.

Authorised by @Soushi888.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(resource): NDO Layer 1 - typed governance rules, classification constraints, OperationalState

2 participants