diff --git a/.github/workflows/validate-shacl.yml b/.github/workflows/validate-shacl.yml index a454b4a..acce7ef 100644 --- a/.github/workflows/validate-shacl.yml +++ b/.github/workflows/validate-shacl.yml @@ -24,9 +24,29 @@ jobs: - name: Validate examples with SHACL run: | - # We check if there are any .ttl files in examples/rdf to validate - if ls examples/rdf/*.ttl 1> /dev/null 2>&1; then - python scripts/validate_rdf.py examples/rdf/*.ttl --shapes shacl/hacp-core.ttl - else + if ! ls examples/rdf/*.ttl 1> /dev/null 2>&1; then echo "No RDF examples found to validate." + exit 0 + fi + + positive_examples=( + examples/rdf/irreversible_task.ttl + examples/rdf/low_confidence_escalation.ttl + examples/rdf/multi_agent_handoff.ttl + ) + positive_culture_examples=( + examples/rdf/culture_profile.ttl + ) + negative_examples=( + examples/rdf/failed_validation.ttl + ) + + python scripts/validate_rdf.py "${positive_examples[@]}" --shapes shacl/hacp-core.ttl + python scripts/validate_rdf.py "${positive_culture_examples[@]}" --shapes shacl/culture-profile.shacl.ttl + + if python scripts/validate_rdf.py "${negative_examples[@]}" --shapes shacl/hacp-core.ttl; then + echo "Expected SHACL failure for negative fixtures, but validation passed." + exit 1 + else + echo "Negative fixtures failed core SHACL as expected." fi diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3ec46..50eb266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,14 @@ All notable changes to this project will be documented in this file. - **Documentation**: `docs/architecture.md`, Decision Matrix in `HACP.md`. - **Examples**: Irreversible tasks, Escalation scenarios, Validation failures. - **CI/CD**: `validate_rdf.py` script and GitHub Workflow. +- **Culture**: Added `schemas/culture-profile.yaml` and `shacl/culture-profile.shacl.ttl`. +- **Culture Example**: Added `examples/culture_profile.yaml` and `examples/rdf/culture_profile.ttl`. ### Changed - Refined `task-manifest.yaml` with collaboration modes and deadlines. - Clarified refusal and model identity requirements in `HACP.md`. +- Added optional `team_id` and `culture_profile_ref` to `schemas/task-manifest.yaml`. +- Added `Culture Policy Binding` requirement in `HACP.md`. +- Updated core SHACL authority-boundary logic to conditionally require `humanApproval` for irreversible executions. +- Updated CI validation to run positive and negative fixture suites separately. diff --git a/COMPLIANCE.md b/COMPLIANCE.md index 6cf7389..579a1cc 100644 --- a/COMPLIANCE.md +++ b/COMPLIANCE.md @@ -18,6 +18,9 @@ See `shacl/*.ttl` — each test is encoded as a SHACL NodeShape. Run `pyshacl` (CI job included) against examples to verify compliance. +- Core controls: `shacl/hacp-core.ttl` +- Culture controls: `shacl/culture-profile.shacl.ttl` + ## Compliance Claim Format Systems claiming compliance should publish a short YAML claim, for example: diff --git a/HACP.md b/HACP.md index 4ab422d..5f1287c 100644 --- a/HACP.md +++ b/HACP.md @@ -31,6 +31,11 @@ Every task MUST be classified before execution along these axes: Unclassified tasks MUST default to **Human-Led, AI-Assisted** handling. Silent delegation is prohibited. +### 3.2 Culture Policy Binding + +Teams MAY publish a Culture Profile artifact that defines decision-rights model, dissent channels, and risk policy tiers. +For tasks where `importance=High` OR `reversibility=Irreversible`, collaboration **MUST** apply the matching Culture Profile `risk_policy` tier rules, including `requires_divergence` when that flag is set. + ## 4. Roles ### 4.1 Human Roles diff --git a/README.md b/README.md index c953899..00d2057 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,11 @@ Goal: make the protocol real and executable so that institutions and vendors can ## Quick start 1. Read the normative spec: `HACP.md` -2. Inspect the schema: `schemas/task-manifest.yaml` -3. Validate an example with SHACL (CI job included): See `.github/workflows/validate-shacl.yml` +2. Inspect the schemas: `schemas/task-manifest.yaml` and `schemas/culture-profile.yaml` +3. Validate examples with SHACL (CI job included): See `.github/workflows/validate-shacl.yml` +4. Review reference fixtures: + - Task fixtures: `examples/rdf/irreversible_task.ttl`, `examples/rdf/low_confidence_escalation.ttl` + - Culture fixture: `examples/rdf/culture_profile.ttl` ## License diff --git a/examples/culture_profile.yaml b/examples/culture_profile.yaml new file mode 100644 index 0000000..a1b15e9 --- /dev/null +++ b/examples/culture_profile.yaml @@ -0,0 +1,32 @@ +team_id: "team-alpha" +decision_rights_model: "RACI" +dissent_channels: + - name: "Minority Report" + preserves_dissent: true + anonymous: false +risk_policy: + tiers: + - name: "low" + requires_divergence: false + requires_minority_report: false + requires_second_order_effects: false + requires_audit_trail: true + escalation_required: false + - name: "high" + requires_divergence: true + requires_minority_report: true + requires_second_order_effects: true + requires_audit_trail: true + escalation_required: true +norms: + - name: "Disagree in writing before commit" + type: "decision" +incentives: + - name: "Escalate uncertainty early" + polarity: "reward" +rituals: + - name: "Incident Review" + cadence: "per-incident" +metrics: + - name: "Escalation Lead Time" + description: "Median time from uncertainty detection to human escalation." diff --git a/examples/rdf/culture_profile.ttl b/examples/rdf/culture_profile.ttl new file mode 100644 index 0000000..005163d --- /dev/null +++ b/examples/rdf/culture_profile.ttl @@ -0,0 +1,25 @@ +@prefix : . +@prefix xsd: . + +:team-alpha-culture a :CultureProfile ; + :teamId "team-alpha" ; + :decisionRightsModel "RACI" ; + :hasDissentChannel :minority-report ; + :hasRiskPolicy :team-alpha-risk-policy . + +:minority-report a :DissentChannel ; + :channelName "Minority Report" ; + :preservesDissent true . + +:team-alpha-risk-policy a :RiskPolicy ; + :hasRiskTier :risk-tier-low, :risk-tier-high . + +:risk-tier-low a :RiskTier ; + :tierName "low" ; + :requiresDivergence false ; + :requiresAuditTrail true . + +:risk-tier-high a :RiskTier ; + :tierName "high" ; + :requiresDivergence true ; + :requiresAuditTrail true . diff --git a/schemas/culture-profile.yaml b/schemas/culture-profile.yaml new file mode 100644 index 0000000..a3b32c4 --- /dev/null +++ b/schemas/culture-profile.yaml @@ -0,0 +1,118 @@ +# YAML schema (human-readable) for Culture Profile +title: HACP Culture Profile +type: object +required: + - team_id + - decision_rights_model + - dissent_channels + - risk_policy + +properties: + team_id: + type: string + description: Unique team identifier (maps to Team entity in higher-level models). + + decision_rights_model: + type: string + enum: [RACI, DACI, RAPID, Bespoke] + description: Team decision-rights topology. + + dissent_channels: + type: array + minItems: 1 + items: + type: object + required: [name, preserves_dissent] + properties: + name: + type: string + description: Human name for the dissent channel. + preserves_dissent: + type: boolean + description: If true, dissent must be durably recorded and linkable to a decision artifact. + anonymous: + type: boolean + default: false + description: If true, this channel supports anonymous dissent. + + risk_policy: + type: object + required: [tiers] + properties: + tiers: + type: array + minItems: 2 + items: + type: object + required: [name, requires_divergence, requires_audit_trail] + properties: + name: + type: string + description: "Tier name (for example: low/high or 1-4)." + requires_divergence: + type: boolean + description: If true, divergence-before-convergence is mandatory at this tier. + requires_minority_report: + type: boolean + default: false + requires_second_order_effects: + type: boolean + default: false + requires_audit_trail: + type: boolean + description: If true, decisions must preserve an auditable rationale chain. + escalation_required: + type: boolean + default: false + description: If true, explicit escalation to a defined authority group is required. + + norms: + type: array + description: Explicit team norms (communication/decision/learning). + items: + type: object + required: [name, type] + properties: + name: + type: string + type: + type: string + enum: [communication, decision, review, learning, safety, execution] + + incentives: + type: array + description: Explicit or implicit incentive signals that affect decision dynamics. + items: + type: object + required: [name, polarity] + properties: + name: + type: string + polarity: + type: string + enum: [reward, penalty, mixed] + + rituals: + type: array + description: Team cadences (retro, design review, incident review, weekly planning). + items: + type: object + required: [name, cadence] + properties: + name: + type: string + cadence: + type: string + description: Example values include weekly, biweekly, monthly, or per-incident. + + metrics: + type: array + description: Aggregate and non-personal signals used to operationalize culture. + items: + type: object + required: [name, description] + properties: + name: + type: string + description: + type: string diff --git a/schemas/task-manifest.yaml b/schemas/task-manifest.yaml index 7364a3f..8101a05 100644 --- a/schemas/task-manifest.yaml +++ b/schemas/task-manifest.yaml @@ -36,6 +36,12 @@ properties: owner: type: string description: Human ID responsible for the task.il or org id) + team_id: + type: string + description: Team identifier used to resolve culture and decision-rights context. + culture_profile_ref: + type: string + description: URI or file reference to the associated Culture Profile artifact. operator: type: string description: Person initiating task diff --git a/shacl/culture-profile.shacl.ttl b/shacl/culture-profile.shacl.ttl new file mode 100644 index 0000000..fafa194 --- /dev/null +++ b/shacl/culture-profile.shacl.ttl @@ -0,0 +1,76 @@ +@prefix : . +@prefix sh: . +@prefix xsd: . + +:CultureProfileShape + a sh:NodeShape ; + sh:targetClass :CultureProfile ; + sh:property [ + sh:path :teamId ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "CultureProfile MUST declare teamId." ; + ] ; + sh:property [ + sh:path :decisionRightsModel ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "CultureProfile MUST declare decisionRightsModel." ; + ] ; + sh:property [ + sh:path :hasDissentChannel ; + sh:minCount 1 ; + sh:message "CultureProfile MUST declare at least one dissent channel." ; + ] ; + sh:property [ + sh:path :hasRiskPolicy ; + sh:minCount 1 ; + sh:message "CultureProfile MUST declare a risk policy." ; + ] . + +:RiskPolicyShape + a sh:NodeShape ; + sh:targetClass :RiskPolicy ; + sh:property [ + sh:path :hasRiskTier ; + sh:minCount 2 ; + sh:message "RiskPolicy MUST define at least two risk tiers." ; + ] . + +:RiskTierShape + a sh:NodeShape ; + sh:targetClass :RiskTier ; + sh:property [ + sh:path :tierName ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "RiskTier MUST have tierName." ; + ] ; + sh:property [ + sh:path :requiresDivergence ; + sh:datatype xsd:boolean ; + sh:minCount 1 ; + sh:message "RiskTier MUST specify requiresDivergence (true/false)." ; + ] ; + sh:property [ + sh:path :requiresAuditTrail ; + sh:datatype xsd:boolean ; + sh:minCount 1 ; + sh:message "RiskTier MUST specify requiresAuditTrail (true/false)." ; + ] . + +:DissentChannelShape + a sh:NodeShape ; + sh:targetClass :DissentChannel ; + sh:property [ + sh:path :channelName ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:message "DissentChannel MUST have channelName." ; + ] ; + sh:property [ + sh:path :preservesDissent ; + sh:datatype xsd:boolean ; + sh:minCount 1 ; + sh:message "DissentChannel MUST specify preservesDissent (true/false)." ; + ] . diff --git a/shacl/hacp-core.ttl b/shacl/hacp-core.ttl index 8703aa1..6b2e317 100644 --- a/shacl/hacp-core.ttl +++ b/shacl/hacp-core.ttl @@ -44,15 +44,14 @@ :AuthorityBoundaryShape a sh:NodeShape ; sh:targetClass :Execution ; sh:message "Execution of irreversible actions requires explicit human approval." ; - sh:property [ - sh:path :reversibility ; - sh:hasValue "Irreversible"^^xsd:string ; - sh:severity sh:Violation ; - ] ; - sh:property [ - sh:path :humanApproval ; - sh:minCount 1 ; - sh:severity sh:Violation ; + sh:sparql [ + sh:message "Execution with reversibility=Irreversible MUST include humanApproval." ; + sh:select """ + SELECT $this WHERE { + $this :reversibility ?rev . + FILTER(str(?rev) = "Irreversible" && NOT EXISTS { $this :humanApproval ?approval }) + } + """ ; ] . :EscalationOnUncertaintyShape a sh:NodeShape ;