fuli-context 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/web/assets/AboutPage-BAD0iPVX.js +1 -0
  2. package/dist/web/assets/AgentConversations-1iGTd4sz.js +2 -0
  3. package/dist/web/assets/AgentHand-D_RafKOM.js +1 -0
  4. package/dist/web/assets/AgentName-B5LKQ09q.js +1 -0
  5. package/dist/web/assets/AgentProfilePage-CioOdkBM.js +1 -0
  6. package/dist/web/assets/AgentsDirectoryPage--1A9nvNA.js +1 -0
  7. package/dist/web/assets/AgentsDirectoryPage-C7OUQH9v.css +1 -0
  8. package/dist/web/assets/ConnectionsPage-Tr2rRVOZ.js +1 -0
  9. package/dist/web/assets/EmployeeRecruitDialog-1QevD4Qv.js +1 -0
  10. package/dist/web/assets/EmployeeWorkbenchPage-B0vQIgwA.js +3 -0
  11. package/dist/web/assets/EmployeeWorkbenchPage-BQes4L1f.css +1 -0
  12. package/dist/web/assets/KnowledgeEditDialog-ClnrfPZi.js +1 -0
  13. package/dist/web/assets/KnowledgeInspector-Te1hJZbx.js +1 -0
  14. package/dist/web/assets/KnowledgeOrganizerPage-CCCrWl6Y.js +1 -0
  15. package/dist/web/assets/KnowledgeOrganizerPage-ccgscrPd.css +1 -0
  16. package/dist/web/assets/KnowledgePage-DGOcBwr8.js +1 -0
  17. package/dist/web/assets/KnowledgeWorkspace-cKn1NCRe.js +4 -0
  18. package/dist/web/assets/NotFoundPage-fetSnp0N.js +1 -0
  19. package/dist/web/assets/OverviewPage-BD4yY2Xz.js +1 -0
  20. package/dist/web/assets/PersonalProfilePage-DGJVP_VB.js +1 -0
  21. package/dist/web/assets/PersonalProjectsPage-D25HYMgD.js +1 -0
  22. package/dist/web/assets/ProjectAgentFirstTask-BcxqmKXQ.js +1 -0
  23. package/dist/web/assets/ProjectAgentsPage-BWDJBESb.js +12 -0
  24. package/dist/web/assets/{ProjectAgentsPage-DNLNjYp-.css → ProjectAgentsPage-DWGkgIsr.css} +1 -1
  25. package/dist/web/assets/ProjectScopePicker-yodM7lF_.js +1 -0
  26. package/dist/web/assets/PublicProjectsPage-CyNxVzW3.js +1 -0
  27. package/dist/web/assets/ReviewPage-Cy0Rt4CO.js +1 -0
  28. package/dist/web/assets/RoundtablesPage-BjVhvwMc.js +1 -0
  29. package/dist/web/assets/SettingsPage-CCZqZtnH.js +1 -0
  30. package/dist/web/assets/SettingsPage-DYz5bTwa.css +1 -0
  31. package/dist/web/assets/SharedJefaReportPage-C56kPciB.js +1 -0
  32. package/dist/web/assets/UiDialog-CvDxg3B1.js +1 -0
  33. package/dist/web/assets/UiDisclosure-D0iMTplF.js +1 -0
  34. package/dist/web/assets/UiSegmented-DIJW6wv3.js +1 -0
  35. package/dist/web/assets/UiSelect-CMR0QqF6.js +1 -0
  36. package/dist/web/assets/WritingTastePage-QgNzssfF.js +1 -0
  37. package/dist/web/assets/agent-pins-BcJ_qr8n.js +1 -0
  38. package/dist/web/assets/agent-work-summary-C-yW7x7A.js +1 -0
  39. package/dist/web/assets/{attention-store-BfxtL9EV.js → attention-store-B1DI8wmd.js} +1 -1
  40. package/dist/web/assets/{catalog-6SN9i3WR.js → catalog-DbXv0_bR.js} +1 -1
  41. package/dist/web/assets/client-CR-W5Wzp.js +1 -0
  42. package/dist/web/assets/console-D7GHOMEC.js +1 -0
  43. package/dist/web/assets/conversation-launcher-settings-Dj2GMzCI.js +1 -0
  44. package/dist/web/assets/{i18n-CCMR4gDS.js → i18n-CVFhT_k7.js} +3 -3
  45. package/dist/web/assets/{identity-C1eN-FSB.js → identity-CM6Dmy00.js} +1 -1
  46. package/dist/web/assets/index-CJ6Jmok9.css +1 -0
  47. package/dist/web/assets/index-CRa3GnYi.js +5 -0
  48. package/dist/web/assets/judgment-types-CJBEyJs1.js +1 -0
  49. package/dist/web/assets/model-WFhQZ3v_.js +1 -0
  50. package/dist/web/assets/{personal-profile-graph-BD1Y6TDS.js → personal-profile-graph-BwCh2M9G.js} +1 -1
  51. package/dist/web/assets/shared-report-CeIvrL9a.js +1 -0
  52. package/dist/web/assets/{agent-work-summary-LeW-Al_S.js → task-evidence-DTiPCs7r.js} +1 -1
  53. package/dist/web/assets/useAgentRoster-DCArHlxK.js +1 -0
  54. package/dist/web/assets/{useApi-CROJJdhE-DlOiu8lP.js → useApi-CROJJdhE-BYhbjaZB.js} +1 -1
  55. package/dist/web/assets/vue-router-DaxrQwX3.js +1 -0
  56. package/dist/web/index.html +16 -12
  57. package/docs/agent-roundtable.md +17 -1
  58. package/docs/employee-agents.md +1 -1
  59. package/docs/judgment-and-autonomy.md +33 -0
  60. package/graph-provider/fuli_graph/ai_review.py +154 -0
  61. package/graph-provider/fuli_graph/common_knowledge.py +12 -0
  62. package/graph-provider/fuli_graph/graph_models.py +14 -0
  63. package/graph-provider/fuli_graph/graph_query.py +217 -0
  64. package/graph-provider/fuli_graph/knowledge_audit.py +6 -0
  65. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +6 -0
  66. package/graph-provider/fuli_graph/knowledge_management.py +23 -0
  67. package/graph-provider/fuli_graph/knowledge_review.py +383 -4
  68. package/graph-provider/fuli_graph/knowledge_review_models.py +61 -2
  69. package/graph-provider/fuli_graph/personal_global_preferences.py +5 -1
  70. package/graph-provider/fuli_graph/preference_conflicts.py +91 -1
  71. package/graph-provider/fuli_graph/project_agent_context_models.py +5 -1
  72. package/graph-provider/fuli_graph/project_agent_delegations.py +118 -0
  73. package/graph-provider/fuli_graph/project_agent_employee_numbers.py +41 -0
  74. package/graph-provider/fuli_graph/project_agent_memory_routes.py +13 -0
  75. package/graph-provider/fuli_graph/project_agent_models.py +1 -0
  76. package/graph-provider/fuli_graph/project_agent_task_models.py +2 -0
  77. package/graph-provider/fuli_graph/project_agent_team_growth.py +2 -2
  78. package/graph-provider/fuli_graph/project_knowledge.py +41 -0
  79. package/graph-provider/fuli_graph/runtime.py +15 -0
  80. package/graph-provider/fuli_graph/store_agent_verification.py +12 -5
  81. package/graph-provider/fuli_graph/store_knowledge.py +98 -58
  82. package/graph-provider/fuli_graph/store_project_agent_attention.py +1 -1
  83. package/graph-provider/fuli_graph/store_project_agent_context.py +52 -4
  84. package/graph-provider/fuli_graph/store_project_agent_coordination_policy.py +16 -1
  85. package/graph-provider/fuli_graph/store_project_agent_task_activity.py +26 -0
  86. package/graph-provider/fuli_graph/store_project_agent_task_staffing.py +5 -3
  87. package/graph-provider/fuli_graph/store_project_agents.py +31 -5
  88. package/graph-provider/fuli_graph/system_agents.py +23 -2
  89. package/graph-provider/fuli_graph/system_hr_identity.py +19 -5
  90. package/graph-provider/pyproject.toml +1 -1
  91. package/npm-shrinkwrap.json +2 -2
  92. package/package.json +3 -2
  93. package/src/agent-roundtable/service.js +34 -5
  94. package/src/agent-roundtable/wake.js +2 -1
  95. package/src/agent-tools/graph-definitions.js +2 -0
  96. package/src/agent-tools/graph-handlers.js +7 -0
  97. package/src/agent-tools/interface-catalog.js +6 -0
  98. package/src/agent-tools/judgment-definitions.js +12 -0
  99. package/src/agents/claude-code/wake.js +7 -0
  100. package/src/agents/codex/wake.js +7 -0
  101. package/src/agents/identity-receipt.js +20 -2
  102. package/src/employees/catalog/tonborg.json +20 -0
  103. package/src/employees/manifest.js +2 -2
  104. package/src/employees/recruitment.js +20 -1
  105. package/src/graphiti/agent-collaboration.js +8 -2
  106. package/src/graphiti/federated-application.js +11 -2
  107. package/src/graphiti/knowledge-review.js +7 -3
  108. package/src/graphiti/project-agent-mapping.js +4 -0
  109. package/src/graphiti/project-agent-task-entry.js +23 -5
  110. package/src/graphiti/project-agent-team-context.js +4 -1
  111. package/src/graphiti/provider-client.js +9 -0
  112. package/src/graphiti/task-entry-preferences.js +17 -4
  113. package/src/graphiti/task-entry-project-scope.js +67 -0
  114. package/src/http/api-router.js +2 -0
  115. package/src/judgment/acceptance.js +91 -0
  116. package/src/judgment/engine.js +168 -0
  117. package/src/judgment/http.js +24 -0
  118. package/src/judgment/model.js +24 -0
  119. package/src/judgment/routing.js +96 -0
  120. package/src/judgment/scheduler.js +25 -0
  121. package/src/judgment/service.js +66 -0
  122. package/src/judgment/store.js +123 -0
  123. package/src/mcp/session-id.js +1 -0
  124. package/src/mcp/tool-annotations.js +4 -2
  125. package/src/server.js +9 -2
  126. package/dist/web/assets/AboutPage-CKq4IRHd.js +0 -1
  127. package/dist/web/assets/AgentConversations-DfXiFvBN.js +0 -2
  128. package/dist/web/assets/AgentHand-BBM-sKh2.js +0 -1
  129. package/dist/web/assets/AgentName-C_6R80nP.js +0 -1
  130. package/dist/web/assets/AgentProfilePage-Cz-i1fRD.js +0 -1
  131. package/dist/web/assets/AgentsDirectoryPage-BI0EFz6y.js +0 -1
  132. package/dist/web/assets/AgentsDirectoryPage-yRWktiin.css +0 -1
  133. package/dist/web/assets/ConnectionsPage-BoC6kXgs.js +0 -1
  134. package/dist/web/assets/EmployeeRecruitDialog-BVyclQAJ.js +0 -1
  135. package/dist/web/assets/EmployeeWorkbenchPage-D-c_Jm2z.css +0 -1
  136. package/dist/web/assets/EmployeeWorkbenchPage-D2GvYraI.js +0 -3
  137. package/dist/web/assets/KnowledgeEditDialog-a0AKWics.js +0 -1
  138. package/dist/web/assets/KnowledgeInspector-DMg9yJZ2.js +0 -1
  139. package/dist/web/assets/KnowledgeOrganizerPage-BgsHM3Qw.js +0 -1
  140. package/dist/web/assets/KnowledgeOrganizerPage-DeL7Jp34.css +0 -1
  141. package/dist/web/assets/KnowledgePage-kn8MqUCO.js +0 -1
  142. package/dist/web/assets/KnowledgeWorkspace-BtRq9fvC.js +0 -4
  143. package/dist/web/assets/NotFoundPage-D-7jLz3o.js +0 -1
  144. package/dist/web/assets/OverviewPage-BHPc-XxY.js +0 -1
  145. package/dist/web/assets/PersonalProfilePage-C00iV0Kx.js +0 -1
  146. package/dist/web/assets/PersonalProjectsPage-CFNkdHSo.js +0 -1
  147. package/dist/web/assets/ProjectAgentFirstTask-CrCWEYgn.js +0 -1
  148. package/dist/web/assets/ProjectAgentsPage-BzztLMH-.js +0 -12
  149. package/dist/web/assets/ProjectScopePicker-CMxn3txv.js +0 -1
  150. package/dist/web/assets/PublicProjectsPage-DQVtvhOS.js +0 -1
  151. package/dist/web/assets/ReviewPage-BZhvyDWc.js +0 -1
  152. package/dist/web/assets/RoundtablesPage-CVebTXQa.js +0 -1
  153. package/dist/web/assets/SettingsPage-1knvfOW6.js +0 -1
  154. package/dist/web/assets/SettingsPage-BmYkLul4.css +0 -1
  155. package/dist/web/assets/SharedJefaReportPage-BCVq8u0P.js +0 -1
  156. package/dist/web/assets/UiDialog-Da1P10f4.js +0 -1
  157. package/dist/web/assets/UiDisclosure-8wE8YuE4.js +0 -1
  158. package/dist/web/assets/UiSegmented-BKZXc8QF.js +0 -1
  159. package/dist/web/assets/UiSelect-D5Xj5Ec4.js +0 -1
  160. package/dist/web/assets/WritingTastePage-B8-etOBn.js +0 -1
  161. package/dist/web/assets/client-CUTkF3t4.js +0 -1
  162. package/dist/web/assets/console-CG8tdtSs.js +0 -1
  163. package/dist/web/assets/conversation-launcher-settings-CyZkLJg7.js +0 -1
  164. package/dist/web/assets/index-kPB0Cf8Q.css +0 -1
  165. package/dist/web/assets/index-sLDKI6R-.js +0 -5
  166. package/dist/web/assets/model-l-sAqtW_.js +0 -1
  167. package/dist/web/assets/shared-report-B46dxlCR.js +0 -1
  168. package/dist/web/assets/useAgentRoster-DdxoKDEE.js +0 -1
  169. package/dist/web/assets/vue-router-DjqEU4W2.js +0 -1
@@ -9,24 +9,28 @@
9
9
  />
10
10
  <link rel="icon" type="image/png" href="/assets/fuli-logo-ER1wQ8Kk.png" />
11
11
  <title>复利 · Context Graph</title>
12
- <script type="module" crossorigin src="/assets/index-sLDKI6R-.js"></script>
13
- <link rel="modulepreload" crossorigin href="/assets/i18n-CCMR4gDS.js">
12
+ <script type="module" crossorigin src="/assets/index-CRa3GnYi.js"></script>
13
+ <link rel="modulepreload" crossorigin href="/assets/i18n-CVFhT_k7.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BDNMzG2s.js">
15
- <link rel="modulepreload" crossorigin href="/assets/client-CUTkF3t4.js">
16
- <link rel="modulepreload" crossorigin href="/assets/console-CG8tdtSs.js">
17
- <link rel="modulepreload" crossorigin href="/assets/UiDialog-Da1P10f4.js">
18
- <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-DlOiu8lP.js">
19
- <link rel="modulepreload" crossorigin href="/assets/vue-router-DjqEU4W2.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/client-CR-W5Wzp.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/console-D7GHOMEC.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/UiDialog-CvDxg3B1.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/useApi-CROJJdhE-BYhbjaZB.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/vue-router-DaxrQwX3.js">
20
20
  <link rel="modulepreload" crossorigin href="/assets/fuli-logo-Bu9WBuQe.js">
21
- <link rel="modulepreload" crossorigin href="/assets/UiSelect-D5Xj5Ec4.js">
22
- <link rel="modulepreload" crossorigin href="/assets/attention-store-BfxtL9EV.js">
23
- <link rel="modulepreload" crossorigin href="/assets/AgentHand-BBM-sKh2.js">
24
- <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-8wE8YuE4.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/UiSelect-CMR0QqF6.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/attention-store-B1DI8wmd.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/AgentHand-D_RafKOM.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/UiDisclosure-D0iMTplF.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/profile-model-BQO4zNs9.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/task-evidence-DTiPCs7r.js">
27
+ <link rel="modulepreload" crossorigin href="/assets/useAgentRoster-DCArHlxK.js">
28
+ <link rel="modulepreload" crossorigin href="/assets/agent-pins-BcJ_qr8n.js">
25
29
  <link rel="modulepreload" crossorigin href="/assets/paths-CDREouhi.js">
26
30
  <link rel="stylesheet" crossorigin href="/assets/client-CIuQ1icy.css">
27
31
  <link rel="stylesheet" crossorigin href="/assets/UiSelect-Bv6R0yr1.css">
28
32
  <link rel="stylesheet" crossorigin href="/assets/AgentHand-Y83Oo36U.css">
29
- <link rel="stylesheet" crossorigin href="/assets/index-kPB0Cf8Q.css">
33
+ <link rel="stylesheet" crossorigin href="/assets/index-CJ6Jmok9.css">
30
34
  </head>
31
35
  <body>
32
36
  <div id="app"></div>
@@ -18,11 +18,27 @@ All tools are on the normal `fuli` MCP server.
18
18
  | `reply_agent_message` | Answer one of them |
19
19
  | `read_agent_thread` | Every message of one thread |
20
20
 
21
- `message_agent` takes `to` (Agent ID or exact name), `body`, the caller's
21
+ `message_agent` takes `to` (Agent ID, exact name, or employee number such as
22
+ `FLA 000001`), `body`, the caller's
22
23
  `taskContextToken`, `projectPath` (the current directory), and optionally `threadId`
23
24
  to continue a thread or `conversation` to choose which of the recipient's
24
25
  conversations answers (`auto` by default, `new`, or a session ID from `find_agents`).
25
26
 
27
+ Each Fuli Agent (FLA) has a permanent employee number within its personal space.
28
+ Numbers start at `000001`, expand beyond six digits when needed, and are never
29
+ reused after archival. Moving between projects or clients preserves the number;
30
+ existing IDs and profile links remain valid. `find_agents` can search by number.
31
+
32
+ Every project has one accountable lead. User requests for a member go through
33
+ that lead. A recorded lead-to-member roundtable delivery restores the member's
34
+ own isolated context and returns its answer to the lead for the final report.
35
+ Changing, deactivating, or archiving a project lead requires a handoff first.
36
+ Delivery requires an active task context. The Provider issues a short-lived
37
+ capability that Fuli passes only to the receiving process, binds to its session,
38
+ and revokes after each attempt. Public message headers do not grant access.
39
+ An answer is attributed to the recipient only after that session redeems the
40
+ capability. Members send their results through the project lead.
41
+
26
42
  ## Delivery
27
43
 
28
44
  1. **Wake.** By default Fuli wakes the recipient in its own client, headlessly and
@@ -1,6 +1,6 @@
1
1
  # Specialist Agents
2
2
 
3
- An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. Two roles are fixed: **Jefa**, the project manager, and **Bole**, the HR Agent. Both exist in every personal space from the start, manage all projects by default, cannot be archived or replaced by another coordinator or `hr` identity, and are never offered for hire. Neither requires a separate runtime package.
3
+ An employee template is a reusable role with an optional FULI-hosted or native workbench. Recruitment reuses the existing durable Project Agent identity and assignment model. Three roles are fixed: **Jefa**, the project manager, **Bole**, the HR Agent, and **Tonborg**, the judgment Agent. They exist in every personal space from the start, manage all projects by default, and are never offered for hire. They require no separate runtime package. See [judgment and autonomy](judgment-and-autonomy.md) for Tonborg's review, feedback, and execution-choice workflow.
4
4
 
5
5
  ## User flow
6
6
 
@@ -0,0 +1,33 @@
1
+ # Tonborg and autonomy
2
+
3
+ Tonborg is the built-in judgment Agent, alongside Jefa and Bole. It has a persistent FLA identity, scoped Agent memory, and a native decision workbench. All Agents can be pinned from their card; pins appear in navigation. The three built-in roles are the initial defaults. Removing a pin, including every pin, survives reloads.
4
+
5
+ ## Choose the amount of autonomy
6
+
7
+ Settings offers three choices, saved immediately with Undo:
8
+
9
+ - **I decide**: Tonborg analyzes and records suggestions.
10
+ - **Shared decisions**: Tonborg applies routine knowledge reviews backed by existing confirmation and accepts verified FULI tasks.
11
+ - **Autonomous**: Tonborg may also review ordinary pending knowledge. Conflicts, sensitive preferences, uncertain evidence, and actions outside existing authority remain for the user.
12
+
13
+ Projects inherit the global policy unless the user sets an exception. Updating FULI does not silently expand existing authority. The selected local client runs the judgment, and quality preference controls its reasoning effort. Logs show the actual model only when the client reports it.
14
+
15
+ ## What a decision does
16
+
17
+ Knowledge review uses bounded batches and an independent review ledger. A successful assessment appears as **Tonborg reviewed**. It does not change human confirmation, authoritative-source confirmation, publication eligibility, or the original evidence. The Provider compares an evidence snapshot within the write transaction; edited or conflicted knowledge loses its effective AI-reviewed disposition. A failing item cannot block the rest of the queue.
18
+
19
+ `assess_agent_action` recommends continuing the current conversation, reusing an authorized conversation, opening a session, or delegating work. It selects only registered, authorized, preflighted executors and available models that satisfy the Agent, assignment, task, and user routing constraints. The host performs and reports the actual launch or resumption. Conversation history is supplied only to a judgment client permitted for that Agent.
20
+
21
+ `assess_task_completion` is called by the accountable lead with its current task context, exact task revision, and artifact revision. Automatic acceptance requires a passed real verification by another Agent. The Provider rechecks the current lead context, task revision, artifact, and verified attempt before completing the FULI task. Jefa's separate human acceptance, publication, permissions, and payments retain their existing boundaries.
22
+
23
+ The console processes bounded automatic review batches while running. Manual review is available in Tonborg's workbench. Other MCP processes share the same local policy and decision database without starting duplicate schedulers. A lease prevents simultaneous decisions for the same project.
24
+
25
+ ## Correct a judgment
26
+
27
+ Use thumbs up or thumbs down with an optional reason. A vote takes one click; reasons can be changed or cleared. The original decision, execution receipts, and feedback history remain auditable. Relevant feedback includes the selected client/model and work kind, so a reasonless vote still identifies the choice being evaluated. Feedback is scoped evidence for later decisions, not a new universal instruction or automatic reversal of an action.
28
+
29
+ Tonborg uses only its own scoped memory and authorized evidence. Client failure, malformed output, stale evidence, or missing authority produces a visible failure or escalation rather than a successful execution claim. Its judgment subprocess has no task tools or MCP access.
30
+
31
+ ## Interfaces
32
+
33
+ The MCP tools are `get_judgment_policy`, `list_judgments`, `review_with_tonborg`, `assess_agent_action`, `assess_task_completion`, `get_agent_pins`, and `pin_agent`. Autonomy changes and feedback remain local user operations. Agent entry context supplies the current policy and workflow guidance; existing sessions should reconnect to discover newly installed tools.
@@ -0,0 +1,154 @@
1
+ """Version-bound AI review evidence helpers.
2
+
3
+ An AI decision is useful only for the exact item snapshot that was supplied to
4
+ the model. Keep the digest deliberately independent from timestamps: edits can
5
+ be visible in the graph before a separate revision or audit timestamp moves.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from datetime import date, datetime
13
+ from typing import Any
14
+
15
+
16
+ _DIRECT_FIELDS = (
17
+ 'item_kind',
18
+ 'item_id',
19
+ 'title',
20
+ 'content',
21
+ 'valid_at',
22
+ 'invalid_at',
23
+ 'confirmation_status',
24
+ 'confirmation_basis_json',
25
+ 'profile_aspect',
26
+ 'preference_scope',
27
+ 'preference_project_id',
28
+ 'preference_agent_id',
29
+ 'inheritance_mode',
30
+ 'current_quadrant',
31
+ 'utility_score',
32
+ 'confidence_score',
33
+ 'qualified_use_count',
34
+ 'distinct_task_count',
35
+ 'negative_evidence_count',
36
+ 'requires_attention',
37
+ 'human_change_version',
38
+ 'attributes_json',
39
+ 'attributes',
40
+ )
41
+
42
+
43
+ def _json_safe(value: Any) -> Any:
44
+ if isinstance(value, (datetime, date)):
45
+ return value.isoformat()
46
+ to_native = getattr(value, 'to_native', None)
47
+ if callable(to_native):
48
+ return _json_safe(to_native())
49
+ if hasattr(value, 'model_dump'):
50
+ return _json_safe(value.model_dump(mode='json'))
51
+ if isinstance(value, dict):
52
+ return {str(key): _json_safe(item) for key, item in value.items()}
53
+ if isinstance(value, set):
54
+ return sorted((_json_safe(item) for item in value), key=repr)
55
+ if isinstance(value, (list, tuple)):
56
+ return [_json_safe(item) for item in value]
57
+ return value
58
+
59
+
60
+ def _canonical_value(item: dict[str, Any], key: str) -> Any:
61
+ value = item.get(key)
62
+ if key == 'attributes' and value is None:
63
+ value = item.get('attributes_json')
64
+ if key == 'attributes_json' and value is None:
65
+ value = item.get('attributes')
66
+ if key in {'attributes', 'attributes_json'}:
67
+ if value in (None, '', '{}'):
68
+ return {}
69
+ if isinstance(value, str):
70
+ try:
71
+ value = json.loads(value)
72
+ except (TypeError, ValueError):
73
+ pass
74
+ return _json_safe(value)
75
+
76
+
77
+ def ai_review_evidence_payload(
78
+ item: dict[str, Any],
79
+ *,
80
+ scope: str,
81
+ personal_project_id: str | None,
82
+ has_conflict: bool,
83
+ ) -> dict[str, Any]:
84
+ """Build the complete evidence identity used by candidate and CAS paths."""
85
+
86
+ payload = {
87
+ 'direct': {
88
+ key: _canonical_value(item, key)
89
+ for key in _DIRECT_FIELDS
90
+ },
91
+ 'scope': scope,
92
+ 'personal_project_id': personal_project_id,
93
+ 'project_ids': sorted({
94
+ str(project_id)
95
+ for project_id in (item.get('project_ids') or [])
96
+ if project_id
97
+ }),
98
+ 'has_conflict': bool(has_conflict),
99
+ }
100
+ return _json_safe(payload)
101
+
102
+
103
+ def ai_review_evidence_token(
104
+ item: dict[str, Any],
105
+ *,
106
+ scope: str,
107
+ personal_project_id: str | None,
108
+ has_conflict: bool,
109
+ ) -> str:
110
+ """Return a deterministic SHA-256 token for the reviewable item snapshot."""
111
+
112
+ encoded = json.dumps(
113
+ ai_review_evidence_payload(
114
+ item,
115
+ scope=scope,
116
+ personal_project_id=personal_project_id,
117
+ has_conflict=has_conflict,
118
+ ),
119
+ ensure_ascii=False,
120
+ sort_keys=True,
121
+ separators=(',', ':'),
122
+ ).encode('utf-8')
123
+ return hashlib.sha256(encoded).hexdigest()
124
+
125
+
126
+ def effective_ai_review(
127
+ item: dict[str, Any],
128
+ *,
129
+ stored_token: str | None,
130
+ stored_assessment: dict[str, Any] | str | None,
131
+ scope: str,
132
+ personal_project_id: str | None,
133
+ has_conflict: bool,
134
+ ) -> dict[str, Any] | None:
135
+ """Expose an assessment only while its exact current evidence still holds."""
136
+
137
+ if not stored_token or item.get('requires_attention') is True or has_conflict:
138
+ return None
139
+ current_token = ai_review_evidence_token(
140
+ item,
141
+ scope=scope,
142
+ personal_project_id=personal_project_id,
143
+ has_conflict=has_conflict,
144
+ )
145
+ if current_token != stored_token:
146
+ return None
147
+ if isinstance(stored_assessment, str):
148
+ try:
149
+ stored_assessment = json.loads(stored_assessment)
150
+ except (TypeError, ValueError):
151
+ return None
152
+ if not isinstance(stored_assessment, dict):
153
+ return None
154
+ return stored_assessment
@@ -330,6 +330,18 @@ def _promotion_query(item_kind):
330
330
  WITH space, parent, canonical, direct_children,
331
331
  collect({{item: duplicate, spec: duplicate_spec}}) AS duplicate_pairs
332
332
  WHERE size(duplicate_pairs) = size($duplicate_specs)
333
+ WITH space, parent, canonical, direct_children, duplicate_pairs,
334
+ [{{kind: $item_kind, item: canonical}}] +
335
+ [pair IN duplicate_pairs |
336
+ {{kind: $item_kind, item: pair.item}}] AS lock_items
337
+ UNWIND lock_items AS lock_item
338
+ WITH space, parent, canonical, direct_children,
339
+ duplicate_pairs, lock_item
340
+ ORDER BY lock_item.kind + ':' + lock_item.item.uuid
341
+ SET lock_item.item.fuli_ai_review_lock_version =
342
+ coalesce(lock_item.item.fuli_ai_review_lock_version, 0) + 1
343
+ WITH space, parent, canonical, direct_children,
344
+ duplicate_pairs, collect(lock_item) AS _locked_items
333
345
  SET canonical.fuli_inheritance_mode = 'descendants',
334
346
  canonical.fuli_inherited_project_ids = []
335
347
  {duplicate_update}
@@ -55,6 +55,13 @@ class GraphNode(StrictModel):
55
55
  last_human_changed_at: datetime | None = None
56
56
  last_agent_viewed_at: datetime | None = None
57
57
  last_agent_reviewed_at: datetime | None = None
58
+ ai_review_evidence_token: str | None = None
59
+ ai_assessment: dict[str, Any] | None = None
60
+ ai_review_scope: str | None = None
61
+ ai_review_project_id: str | None = None
62
+ ai_reviewed_at: datetime | None = None
63
+ ai_review_id: str | None = None
64
+ ai_review_decision_id: str | None = None
58
65
  utility_score: float = Field(default=0, ge=0, le=1)
59
66
  confidence_score: float = Field(default=0.5, ge=0, le=1)
60
67
  qualified_use_count: int = Field(default=0, ge=0)
@@ -109,6 +116,13 @@ class GraphEdge(StrictModel):
109
116
  last_human_changed_at: datetime | None = None
110
117
  last_agent_viewed_at: datetime | None = None
111
118
  last_agent_reviewed_at: datetime | None = None
119
+ ai_review_evidence_token: str | None = None
120
+ ai_assessment: dict[str, Any] | None = None
121
+ ai_review_scope: str | None = None
122
+ ai_review_project_id: str | None = None
123
+ ai_reviewed_at: datetime | None = None
124
+ ai_review_id: str | None = None
125
+ ai_review_decision_id: str | None = None
112
126
  utility_score: float = Field(default=0, ge=0, le=1)
113
127
  confidence_score: float = Field(default=0.5, ge=0, le=1)
114
128
  qualified_use_count: int = Field(default=0, ge=0)
@@ -4,6 +4,7 @@ from .graph_projection import (
4
4
  management_projection,
5
5
  personal_project_projection,
6
6
  )
7
+ from .ai_review import effective_ai_review
7
8
  from .graph_models import GraphEdge, GraphNode, GraphResult
8
9
  from .models import (
9
10
  ConfirmationBasis,
@@ -82,6 +83,7 @@ async def read_graph(
82
83
  assignments = await _read_assignments(store, space_id, item_ids)
83
84
  references = await read_project_references(store, space_id, item_ids)
84
85
  conflicts = await read_knowledge_conflicts(store, space_id, item_ids)
86
+ preference_conflicts = await _read_preference_conflicts(store, space_id, item_ids)
85
87
  audits = await read_knowledge_audits(store, space_id, item_ids)
86
88
  knowledge_nodes = [
87
89
  _graph_node(
@@ -92,6 +94,9 @@ async def read_graph(
92
94
  references,
93
95
  conflicts,
94
96
  audits,
97
+ preference_conflicts=preference_conflicts,
98
+ review_scope='project' if project else 'all',
99
+ personal_project_id=personal_project_id,
95
100
  )
96
101
  for record in node_records
97
102
  ]
@@ -104,6 +109,9 @@ async def read_graph(
104
109
  references,
105
110
  conflicts,
106
111
  audits,
112
+ preference_conflicts=preference_conflicts,
113
+ review_scope='project' if project else 'all',
114
+ personal_project_id=personal_project_id,
107
115
  )
108
116
  for record in edge_records
109
117
  ]
@@ -229,6 +237,13 @@ def _node_query(project_scoped: bool, paginated: bool = False) -> str:
229
237
  node.fuli_last_human_changed_at AS last_human_changed_at,
230
238
  node.fuli_last_agent_viewed_at AS last_agent_viewed_at,
231
239
  node.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
240
+ node.fuli_ai_review_evidence_token AS ai_review_evidence_token,
241
+ node.fuli_ai_assessment_json AS ai_assessment_json,
242
+ node.fuli_ai_review_scope AS ai_review_scope,
243
+ node.fuli_ai_review_project_id AS ai_review_project_id,
244
+ node.fuli_ai_reviewed_at AS ai_reviewed_at,
245
+ node.fuli_ai_review_id AS ai_review_id,
246
+ node.fuli_ai_review_decision_id AS ai_review_decision_id,
232
247
  coalesce(node.fuli_utility_score, 0.0) AS utility_score,
233
248
  coalesce(node.fuli_confidence_score, 0.5) AS confidence_score,
234
249
  coalesce(node.fuli_qualified_use_count, 0)
@@ -345,6 +360,13 @@ def _edge_query(project_scoped: bool, paginated: bool = False) -> str:
345
360
  edge.fuli_last_human_changed_at AS last_human_changed_at,
346
361
  edge.fuli_last_agent_viewed_at AS last_agent_viewed_at,
347
362
  edge.fuli_last_agent_reviewed_at AS last_agent_reviewed_at,
363
+ edge.fuli_ai_review_evidence_token AS ai_review_evidence_token,
364
+ edge.fuli_ai_assessment_json AS ai_assessment_json,
365
+ edge.fuli_ai_review_scope AS ai_review_scope,
366
+ edge.fuli_ai_review_project_id AS ai_review_project_id,
367
+ edge.fuli_ai_reviewed_at AS ai_reviewed_at,
368
+ edge.fuli_ai_review_id AS ai_review_id,
369
+ edge.fuli_ai_review_decision_id AS ai_review_decision_id,
348
370
  coalesce(edge.fuli_utility_score, 0.0) AS utility_score,
349
371
  coalesce(edge.fuli_confidence_score, 0.5) AS confidence_score,
350
372
  coalesce(edge.fuli_qualified_use_count, 0)
@@ -462,6 +484,31 @@ async def _read_assignments(store, space_id: str, item_ids: list[str]):
462
484
  return result
463
485
 
464
486
 
487
+ async def _read_preference_conflicts(store, space_id: str, item_ids: list[str]):
488
+ if not item_ids:
489
+ return {}
490
+ records, _, _ = await store.runtime.driver.execute_query(
491
+ '''
492
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
493
+ [:HAS_PREFERENCE_CONFLICT]->
494
+ (conflict:FuliPreferenceConflict {status: 'ai_pending'})
495
+ WHERE conflict.left_item_id IN $item_ids
496
+ OR conflict.right_item_id IN $item_ids
497
+ RETURN conflict
498
+ ''',
499
+ space_id=space_id,
500
+ item_ids=item_ids,
501
+ routing_='r',
502
+ )
503
+ result = {}
504
+ for record in records:
505
+ value = dict(record['conflict'])
506
+ for item_id in (value.get('left_item_id'), value.get('right_item_id')):
507
+ if item_id in item_ids:
508
+ result.setdefault(item_id, []).append(value)
509
+ return result
510
+
511
+
465
512
  def _graph_node(
466
513
  record,
467
514
  evidence,
@@ -470,12 +517,27 @@ def _graph_node(
470
517
  references,
471
518
  conflicts,
472
519
  audits=None,
520
+ *,
521
+ preference_conflicts=None,
522
+ review_scope: str = 'all',
523
+ personal_project_id: str | None = None,
473
524
  ) -> GraphNode:
474
525
  episode_ids = record.get('episodes') or []
475
526
  status, basis, confidence, confirmation_explicit = _confirmation_projection(
476
527
  record
477
528
  )
478
529
  attributes = json_object(record.get('attributes_json'))
530
+ ai_assessment = _effective_ai_projection(
531
+ record,
532
+ item_kind='entity',
533
+ evidence=evidence,
534
+ assignments=assignments,
535
+ references=references,
536
+ conflicts=conflicts,
537
+ preference_conflicts=preference_conflicts or {},
538
+ review_scope=review_scope,
539
+ personal_project_id=personal_project_id,
540
+ )
479
541
  return GraphNode(
480
542
  id=record['id'],
481
543
  name=record['name'],
@@ -516,6 +578,14 @@ def _graph_node(
516
578
  last_agent_reviewed_at=_native_datetime(
517
579
  record.get('last_agent_reviewed_at')
518
580
  ),
581
+ ai_review_evidence_token=record.get('ai_review_evidence_token'),
582
+ ai_assessment=ai_assessment,
583
+ ai_review_scope=record.get('ai_review_scope'),
584
+ ai_review_project_id=record.get('ai_review_project_id'),
585
+ ai_reviewed_at=_native_datetime(record.get('ai_reviewed_at'))
586
+ if ai_assessment is not None else None,
587
+ ai_review_id=record.get('ai_review_id'),
588
+ ai_review_decision_id=record.get('ai_review_decision_id'),
519
589
  utility_score=float(record.get('utility_score') or 0),
520
590
  confidence_score=confidence,
521
591
  qualified_use_count=int(record.get('qualified_use_count') or 0),
@@ -550,12 +620,27 @@ def _graph_edge(
550
620
  references,
551
621
  conflicts,
552
622
  audits=None,
623
+ *,
624
+ preference_conflicts=None,
625
+ review_scope: str = 'all',
626
+ personal_project_id: str | None = None,
553
627
  ) -> GraphEdge:
554
628
  episode_ids = record.get('episodes') or []
555
629
  status, basis, confidence, confirmation_explicit = _confirmation_projection(
556
630
  record
557
631
  )
558
632
  attributes = json_object(record.get('attributes_json'))
633
+ ai_assessment = _effective_ai_projection(
634
+ record,
635
+ item_kind='relationship',
636
+ evidence=evidence,
637
+ assignments=assignments,
638
+ references=references,
639
+ conflicts=conflicts,
640
+ preference_conflicts=preference_conflicts or {},
641
+ review_scope=review_scope,
642
+ personal_project_id=personal_project_id,
643
+ )
559
644
  return GraphEdge(
560
645
  id=record['id'],
561
646
  source=record['source'],
@@ -598,6 +683,14 @@ def _graph_edge(
598
683
  last_agent_reviewed_at=_native_datetime(
599
684
  record.get('last_agent_reviewed_at')
600
685
  ),
686
+ ai_review_evidence_token=record.get('ai_review_evidence_token'),
687
+ ai_assessment=ai_assessment,
688
+ ai_review_scope=record.get('ai_review_scope'),
689
+ ai_review_project_id=record.get('ai_review_project_id'),
690
+ ai_reviewed_at=_native_datetime(record.get('ai_reviewed_at'))
691
+ if ai_assessment is not None else None,
692
+ ai_review_id=record.get('ai_review_id'),
693
+ ai_review_decision_id=record.get('ai_review_decision_id'),
601
694
  utility_score=float(record.get('utility_score') or 0),
602
695
  confidence_score=confidence,
603
696
  qualified_use_count=int(record.get('qualified_use_count') or 0),
@@ -658,3 +751,127 @@ def _confirmation_projection(record) -> tuple[
658
751
  and basis is not None
659
752
  )
660
753
  return status, basis, confidence, explicit
754
+
755
+
756
+ def _field_value(value, name: str, default=None):
757
+ if isinstance(value, dict):
758
+ return value.get(name, default)
759
+ return getattr(value, name, default)
760
+
761
+
762
+ def _effective_ai_projection(
763
+ record,
764
+ *,
765
+ item_kind: str,
766
+ evidence,
767
+ assignments,
768
+ references,
769
+ conflicts,
770
+ preference_conflicts,
771
+ review_scope: str,
772
+ personal_project_id: str | None,
773
+ ) -> dict | None:
774
+ # The display scope may be broader than the scope used to produce the
775
+ # assessment. Validate against the persisted review scope so a
776
+ # project-bound token remains effective in an authorized all-space view.
777
+ stored_review_scope = record.get('ai_review_scope')
778
+ stored_review_project_id = record.get('ai_review_project_id')
779
+ validation_scope = stored_review_scope or review_scope
780
+ validation_project_id = (
781
+ stored_review_project_id
782
+ if stored_review_project_id is not None
783
+ else personal_project_id
784
+ )
785
+ if (
786
+ stored_review_project_id is not None
787
+ and review_scope in {'project', 'preferences_project'}
788
+ and personal_project_id != stored_review_project_id
789
+ ):
790
+ return None
791
+ project_scoped_review = validation_scope in {'project', 'preferences_project'}
792
+ item_id = record['id']
793
+ project_ids = {
794
+ str(project_id)
795
+ for project_id in (record.get('project_ids') or [])
796
+ if project_id
797
+ }
798
+ preference_project_id = record.get('preference_project_id')
799
+ if preference_project_id:
800
+ project_ids.add(str(preference_project_id))
801
+ for assignment in assignments.get(item_id, []):
802
+ project_id = _field_value(assignment, 'project_id')
803
+ if project_id:
804
+ project_ids.add(str(project_id))
805
+ for reference in references.get(item_id, []):
806
+ project_id = _field_value(reference, 'project_id')
807
+ if project_id and _field_value(reference, 'status') == 'active':
808
+ project_ids.add(str(project_id))
809
+ for episode_id in record.get('episodes') or []:
810
+ project_id = _field_value(evidence.get(episode_id), 'personal_project_id')
811
+ if project_id:
812
+ project_ids.add(str(project_id))
813
+
814
+ scoped_conflict = False
815
+ for conflict in conflicts.get(item_id, []):
816
+ if _field_value(conflict, 'status') != 'pending':
817
+ continue
818
+ if not project_scoped_review or validation_project_id is None:
819
+ scoped_conflict = True
820
+ break
821
+ target_project_id = _field_value(conflict, 'target_project_id')
822
+ source_project_id = _field_value(conflict, 'source_project_id')
823
+ if validation_project_id in {target_project_id, source_project_id}:
824
+ scoped_conflict = True
825
+ break
826
+ if not scoped_conflict:
827
+ for conflict in preference_conflicts.get(item_id, []):
828
+ if _field_value(conflict, 'status') != 'ai_pending':
829
+ continue
830
+ if not project_scoped_review or validation_project_id is None:
831
+ scoped_conflict = True
832
+ break
833
+ if _field_value(conflict, 'preference_project_id') == validation_project_id:
834
+ scoped_conflict = True
835
+ break
836
+
837
+ item = {
838
+ 'item_kind': item_kind,
839
+ 'item_id': item_id,
840
+ 'title': (
841
+ record.get('name') or item_id
842
+ if item_kind == 'entity'
843
+ else record.get('type') or 'RELATES_TO'
844
+ ),
845
+ 'content': (
846
+ record.get('summary') or ''
847
+ if item_kind == 'entity'
848
+ else record.get('fact') or ''
849
+ ),
850
+ 'valid_at': record.get('valid_at'),
851
+ 'invalid_at': record.get('invalid_at'),
852
+ 'confirmation_status': record.get('confirmation_status'),
853
+ 'confirmation_basis_json': record.get('confirmation_basis_json'),
854
+ 'profile_aspect': record.get('profile_aspect'),
855
+ 'preference_scope': record.get('preference_scope'),
856
+ 'preference_project_id': preference_project_id,
857
+ 'preference_agent_id': record.get('preference_agent_id'),
858
+ 'inheritance_mode': record.get('inheritance_mode') or 'local_only',
859
+ 'current_quadrant': record.get('current_quadrant') or 'known_known',
860
+ 'utility_score': record.get('utility_score'),
861
+ 'confidence_score': record.get('confidence_score'),
862
+ 'qualified_use_count': record.get('qualified_use_count'),
863
+ 'distinct_task_count': record.get('distinct_task_count'),
864
+ 'negative_evidence_count': record.get('negative_evidence_count'),
865
+ 'requires_attention': record.get('requires_attention') is True,
866
+ 'human_change_version': record.get('human_change_version') or 0,
867
+ 'attributes_json': record.get('attributes_json'),
868
+ 'project_ids': sorted(project_ids),
869
+ }
870
+ return effective_ai_review(
871
+ item,
872
+ stored_token=record.get('ai_review_evidence_token'),
873
+ stored_assessment=record.get('ai_assessment_json'),
874
+ scope=validation_scope,
875
+ personal_project_id=validation_project_id,
876
+ has_conflict=scoped_conflict,
877
+ )
@@ -630,6 +630,8 @@ def _human_change_query(item_kind: str) -> str:
630
630
  return f'''
631
631
  MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
632
632
  {_item_match(item_kind)}
633
+ SET item.fuli_ai_review_lock_version =
634
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
633
635
  SET item.fuli_human_edited = true,
634
636
  item.fuli_human_change_version =
635
637
  coalesce(item.fuli_human_change_version, 0) + 1,
@@ -797,6 +799,8 @@ def _knowledge_feedback_query(item_kind: str) -> str:
797
799
  MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
798
800
  {_item_match(item_kind)}
799
801
  WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
802
+ SET item.fuli_ai_review_lock_version =
803
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
800
804
  MERGE (audit:FuliKnowledgeAudit {{id: $audit_id}})
801
805
  ON CREATE SET audit.space_id = $space_id,
802
806
  audit.item_id = $item_id,
@@ -850,6 +854,8 @@ def _usage_score_update_query(item_kind: str) -> str:
850
854
  return f'''
851
855
  {_item_match(item_kind)}
852
856
  WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
857
+ SET item.fuli_ai_review_lock_version =
858
+ coalesce(item.fuli_ai_review_lock_version, 0) + 1
853
859
  SET item.fuli_utility_score = $utility_score,
854
860
  item.fuli_confidence_score = $confidence_score,
855
861
  item.fuli_qualified_use_count = $qualified_use_count,