{
  "schema_version": "1.0",
  "baseline": "A-document-access-v1",
  "updated_at": "2026-04-20",
  "owner": "session_a",
  "purpose_en": "Field specification for the user/profile + document visibility system of the console. Paired with the Universal Document Shell. Backing in this batch: browser localStorage. Server-side persistence + real authentication deferred.",
  "purpose_th": "Schema ของระบบ user profile + visibility ของเอกสาร · ใช้งานคู่กับ Universal Document Shell · รอบนี้เก็บใน localStorage ของ browser เท่านั้น · ยังไม่มี auth backend จริง",
  "honest_note_en": "This is a SCAFFOLD. There is no auth backend. No password. No session validation. No server-side enforcement. Any user can change identity by editing localStorage. Do NOT deploy as-is for security-sensitive content.",
  "honest_note_th": "โครงร่างเท่านั้น · ไม่มี backend · ไม่มี password · ไม่มีการตรวจสอบฝั่ง server · user สามารถเปลี่ยน identity ได้ด้วยการแก้ localStorage · ห้ามนำไป deploy จริงสำหรับเนื้อหาที่ต้องการความปลอดภัย",
  "storage_keys": {
    "current_user": "ds.user.current",
    "user_roster":  "ds.user.roster"
  },
  "user_profile_fields": {
    "profile_id": {
      "type": "string",
      "format": "u-<base36-timestamp>-<5char>",
      "required": true,
      "stability": "immutable",
      "description_en": "Client-generated stable id for a profile. Used as the key in the roster map."
    },
    "email": {
      "type": "string",
      "format": "RFC-5322",
      "required": true,
      "description_en": "Primary identity. Case-insensitive match. Uniqueness enforced client-side only."
    },
    "display_name": {
      "type": "string",
      "required": false,
      "description_en": "Optional human name for the header badge. Defaults to the local-part of the email."
    },
    "role": {
      "type": "enum",
      "values": ["viewer", "editor", "reviewer", "admin", "governance", "external"],
      "required": true,
      "default": "viewer",
      "description_en": "Informational role. Shell uses role only to select a sensible default visibility policy. No enforcement."
    },
    "created_at": {
      "type": "string",
      "format": "ISO-8601",
      "required": true
    },
    "last_seen_at": {
      "type": "string",
      "format": "ISO-8601",
      "required": true
    },
    "visible_groups": {
      "type": "array<group_id>",
      "required": true,
      "default": ["start", "knowledge", "planning", "runtime", "operations", "journey"],
      "description_en": "Groups the user is allowed to see in the main portal and group-filtered views. Empty array = nothing visible."
    },
    "hidden_groups": {
      "type": "array<group_id>",
      "required": false,
      "default": [],
      "description_en": "Explicit deny list. Wins over visible_groups if an id appears in both."
    },
    "visible_documents": {
      "type": "array<doc_id>",
      "required": false,
      "description_en": "Explicit allow list for documents. When present, visibility is narrowed to this list AND must also pass the group filter."
    },
    "hidden_documents": {
      "type": "array<doc_id>",
      "required": false,
      "default": [],
      "description_en": "Explicit deny list for documents. Wins over visible_documents."
    },
    "restricted_documents": {
      "type": "array<doc_id>",
      "required": false,
      "default": [],
      "description_en": "Documents where the card is visible but content is hidden. Shell shows a 'Restricted' banner. Useful for redacted previews."
    },
    "preferred_language": {
      "type": "enum",
      "values": ["th", "en", "both"],
      "required": true,
      "default": "both",
      "description_en": "Reading language. 'both' shows TH and EN together where available."
    },
    "stakeholder_tags": {
      "type": "array<string>",
      "required": false,
      "default": [],
      "description_en": "User-selected stakeholder tags that drive the Stakeholder filter default state on landing."
    },
    "policy_note": {
      "type": "string",
      "required": false,
      "description_en": "Free-text rationale for the visibility policy attached to this profile. Displayed on the shell access panel."
    }
  },
  "visibility_resolution": {
    "algorithm": [
      "1. Load current user from ds.user.current. If null → use 'anonymous' profile (all groups visible · no hidden docs · access=preview).",
      "2. For the target document: compute group_id from the Document Groups audit (document-groups.html).",
      "3. If group_id in user.hidden_groups → HIDDEN (group). Shell renders nothing for the doc card · search filters it out.",
      "4. If user.visible_groups is empty OR does not contain group_id → HIDDEN (group).",
      "5. If doc_id in user.hidden_documents → HIDDEN (doc).",
      "6. If user.visible_documents is present and does not contain doc_id → HIDDEN (doc).",
      "7. If doc_id in user.restricted_documents → RESTRICTED. Card visible · content masked with a banner explaining access state.",
      "8. Otherwise → VISIBLE."
    ],
    "precedence": "hidden_groups > hidden_documents > restricted_documents > visible_documents > visible_groups",
    "performance_note": "Resolution is O(1) per document with Set-backed lookups. Safe to run on every render."
  },
  "honest_limits": [
    "No server-side enforcement. Client can bypass by editing localStorage.",
    "No password or session validation.",
    "Role field is informational only.",
    "Stakeholder tags are UI hints, not access keys.",
    "Visibility decisions are not audited anywhere.",
    "Cross-device sync absent. Each browser keeps its own roster."
  ],
  "planned_upgrades": [
    "Real authentication (OAuth or SSO) · Batch 7+",
    "Server-side visibility enforcement",
    "Group-level and document-level policies driven by a rules engine",
    "Audit trail on visibility decisions (who accessed what, when)",
    "Policy inheritance (e.g., 'kb.sensitive' wildcard)",
    "Row-level redaction (partial field hiding within a doc)"
  ],
  "cross_references": {
    "examples":       "docs/assets/access/user_profile_examples.json",
    "matrix":         "docs/assets/access/document_visibility_matrix.json",
    "labels":         "docs/assets/access/access_state_labels.json",
    "planning_doc":   "docs/planning/document-access-model.html",
    "notes_schema":   "docs/assets/notes/document_note_schema.json"
  }
}
