Skip to content

feat: SoA tree view channel plugin#555

Open
data-bot-coasys wants to merge 6 commits intodevfrom
feature/soa-tree-view
Open

feat: SoA tree view channel plugin#555
data-bot-coasys wants to merge 6 commits intodevfrom
feature/soa-tree-view

Conversation

@data-bot-coasys
Copy link
Contributor

@data-bot-coasys data-bot-coasys commented Mar 7, 2026

SoA Tree View

A new Flux channel plugin that renders State of Affairs (SoA) nodes as a collapsible tree view.

What is SoA?

SoA is an ontology for knowledge representation in AD4M (see ad4m#704). Every node has a modality (belief, observation, intention, vision, plan, skill) and properties like confidence, status, and tags. Nodes connect via typed relationships (supports, contradicts, requires, etc.).

Features

  • 🌳 Collapsible tree — parent-child hierarchy via soa://rel_parent links
  • 🔭💭🎯🌟📋🛠️ Modality icons — at-a-glance node type identification
  • 📊 Confidence bars — visual 0-100% confidence indicator
  • 🏷️ Status badges — color-coded (green=active, blue=completed, red=blocked)
  • 🔗 Relationship badges — shows supports, contradicts, requires, enables, etc.
  • 🏷️ Tag pills — categorization display
  • Priority indicators — P1-P5 badges

Architecture

Follows the standard Flux view pattern:

  • Preact custom element via @coasys/flux-react-web
  • Queries raw soa:// links from the perspective
  • Builds the tree in memory (no external dependencies)
  • Uses j-text and flux-ui CSS variables for consistent styling

Read-only for now

Editing will follow in a separate PR.

How to test

  1. Create a perspective with SoA SHACL schema installed
  2. Add the SoA Tree plugin to a channel
  3. Navigate to the channel to see the tree view

Related: ad4m#704 (SoA ontology + static soa language)

Summary by CodeRabbit

  • New Features

    • Added SoA Tree View: hierarchical State of Affairs UI with expandable nodes, status/priority badges, confidence bars, tags, relationship links, and loading/error/empty states; selectable as a channel view.
  • Chores

    • Introduced a new tree-view package, added its runtime integration for dynamic loading, and registered the view option in the app.

Renders State of Affairs nodes from AD4M perspectives as a
collapsible tree view with:
- Modality icons (🔭 observation, 💭 belief, 🎯 intention, etc.)
- Confidence progress bars
- Status badges (color-coded: active/completed/blocked)
- Priority indicators
- Tag pills
- Relationship badges (supports, contradicts, requires, etc.)
- Auto-discovery of parent-child hierarchy via soa://rel_parent

Read-only for now - editing to follow.

Follows the same architecture as table-view:
- Preact custom element via @coasys/flux-react-web
- Queries raw soa:// links and builds tree in memory
- Uses j-text and flux-ui CSS variables for consistent styling
@netlify
Copy link

netlify bot commented Mar 7, 2026

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit c55b086
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/69af1a1c9f161a0007bcc21b
😎 Deploy Preview https://deploy-preview-555--fluxsocial-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47bb2f66-cbde-4862-bd4c-aa642a814ffe

📥 Commits

Reviewing files that changed from the base of the PR and between 78626a6 and e6af3e1.

📒 Files selected for processing (1)
  • packages/api/src/npmApi.ts

📝 Walkthrough

Walkthrough

Adds a new SoA tree view package and integrates it into the app: new Preact components and styles, package/vite configs, app dependency and module declaration, ChannelView enum addition and view option, and dynamic import support.

Changes

Cohort / File(s) Summary
App integration
app/package.json, app/src/utils/fetchFluxApp.ts, app/src/constants/index.ts, app/src/modules.ts
Added dependency @coasys/flux-soa-tree-view; registered it for dynamic import; added a ChannelView member and view option; declared ambient module.
Offline registry
packages/api/src/npmApi.ts
Added flux-soa-tree-view to the static offline FluxApp list.
Type definitions
packages/types/src/index.ts
Added SoATree = 'flux://has_soa_tree_view' to ChannelView enum.
New package config & build
views/soa-tree-view/package.json, views/soa-tree-view/vite.config.ts
Added package manifest and Vite config for building a Preact library with CSS injection and library output.
Entry & wrapper
views/soa-tree-view/src/main.ts, views/soa-tree-view/src/App.tsx, views/soa-tree-view/src/App.module.css
Exposed App as a custom element (props: perspective, agent, source); implemented App component and container/error styles.
Tree view and types
views/soa-tree-view/src/components/SoATreeView.tsx, views/soa-tree-view/src/components/SoATreeView.module.css
New SoATreeView component: queries perspective links for soa:// predicates, parses LinkExpression data, builds SoANodeData trees, handles loading/error/empty states, and exports SoANodeData.
Node component & styles
views/soa-tree-view/src/components/SoANode.tsx, views/soa-tree-view/src/components/SoANode.module.css
Added recursive SoANode component with expandable headers, badges, confidence bar, tags, relationships, depth-based indentation, and corresponding CSS.

Sequence Diagram(s)

sequenceDiagram
    participant App as App (CustomElement)
    participant SoATreeView as SoATreeView
    participant Perspective as Perspective (AD4M)
    participant SoANode as SoANode

    App->>SoATreeView: render(perspective, agent, source)
    SoATreeView->>Perspective: queryLinks(filter: soa://*)
    Perspective-->>SoATreeView: LinkExpression[]
    SoATreeView->>SoATreeView: parse links & buildTree()
    SoATreeView->>SoANode: render(rootNode, depth=0)
    SoANode->>SoANode: toggle expand / render details
    SoANode->>SoANode: render children (recursive)
    SoANode-->>SoATreeView: subtree rendered
    SoATreeView-->>App: tree rendered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I nibble at links and weave a tree,
Tiny branches sprout for all to see,
Badges gleam and tags hop free,
Nodes unfold in tidy rows,
Hooray — the SoA garden grows!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: SoA tree view channel plugin' accurately and concisely describes the main change: adding a new Flux channel plugin that displays State of Affairs nodes as a collapsible tree view.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/soa-tree-view

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
views/soa-tree-view/package.json (2)

5-5: Consider filling in the author field.

The author field is currently an empty string. Consider adding the appropriate author information for package attribution.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/package.json` at line 5, The package.json currently has
an empty "author" field; update the "author" value to include the package owner
(e.g., a person name and optional email or organization) by populating the
"author" property in package.json (the "author" key) with the appropriate string
or object format so the package has correct attribution.

12-12: Placeholder test script noted.

The test script currently exits with an error. Per the PR description, this is a read-only view with editing to follow in a separate PR. Consider adding at least basic unit tests for the tree-building logic in a follow-up.

Would you like me to help create a GitHub issue to track adding tests for this package?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/package.json` at line 12, The package.json currently
contains a placeholder failing "test" script; update the "test" script in
package.json to a meaningful default (e.g., a no-op that exits 0 or a command
that runs the chosen test runner) so CI won't fail, and create a follow-up
GitHub issue to add unit tests for the tree-building logic (track
functions/classes responsible for tree construction such as the module that
builds/parses the SOA tree). Ensure the issue references the package.json "test"
script and the specific tree-building module so adding tests (e.g., for
buildTree/parseTree) is tracked.
views/soa-tree-view/src/components/SoATreeView.tsx (2)

25-34: Unused constant: PROPERTY_PREDICATES is defined but never referenced.

This constant is not used anywhere in the file. Consider removing it to avoid confusion, or add a comment if it's intended for future use.

🧹 Proposed fix
-const PROPERTY_PREDICATES = [
-  'soa://title',
-  'soa://modality',
-  'soa://description',
-  'soa://confidence',
-  'soa://status',
-  'soa://tags',
-  'soa://priority',
-  'soa://source',
-];
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` around lines 25 - 34,
PROPERTY_PREDICATES in SoATreeView.tsx is defined but never used; either remove
the constant to eliminate dead code or keep it with an explanatory comment or
export if it's intended for future use. Locate the const PROPERTY_PREDICATES
declaration and either delete it or add a TODO comment explaining its planned
usage (or change it to an exported symbol if other modules will reference it).

144-147: Consider using server-side filtering with perspective.get() and LinkQuery.

The current approach fetches all links and filters client-side, which may be inefficient for large perspectives. AD4M's LinkQuery supports predicate filtering, and the codebase uses it widely. However, the API appears to support exact predicate matching (e.g., new LinkQuery({ predicate: 'ad4m://has_child' })). For filtering by soa:// prefix, you can either:

  1. Query each predicate individually using the PROPERTY_PREDICATES and RELATIONSHIP_PREDICATES arrays, or
  2. Check if AD4M supports pattern matching in the predicate field for prefix queries.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` around lines 144 - 147,
The code in SoATreeView.tsx currently calls perspective.queryLinks({}) and
filters client-side for predicates starting with "soa://"; change this to use
server-side filtering via LinkQuery/perspective.get to avoid fetching all links.
Either iterate the PROPERTY_PREDICATES and RELATIONSHIP_PREDICATES arrays and
call perspective.get(new LinkQuery({ predicate: '<each-predicate>' })) to
collect matching links, or (if supported) use a LinkQuery with a predicate
pattern/prefix for "soa://". Replace the allLinks = await
perspective.queryLinks({}) + client filter logic with the server-side queries
and merge their results into soaLinks.
views/soa-tree-view/src/App.tsx (1)

5-5: Use the public export instead of internal package paths.

AgentClient is exported from @coasys/ad4m and should be imported from the public entry point instead of relying on internal package structure. This is already used elsewhere in the codebase and is consistent with how PerspectiveProxy is imported in the same file (line 2).

♻️ Proposed fix
-import { AgentClient } from '@coasys/ad4m/lib/src/agent/AgentClient';
+import { AgentClient } from '@coasys/ad4m';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/App.tsx` at line 5, Replace the internal-package
import of AgentClient with the public export: change the import "import {
AgentClient } from '@coasys/ad4m/lib/src/agent/AgentClient';" to import
AgentClient from the package entry point (same style as PerspectiveProxy), i.e.,
import AgentClient from '@coasys/ad4m', so the file uses the public API surface
instead of internal paths and stays consistent with PerspectiveProxy.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@views/soa-tree-view/src/App.tsx`:
- Line 14: The component currently returns a raw string when missing perspective
or agent (the line checking perspective?.uuid and agent); replace that plain
string return with a proper JSX error element consistent with SoATreeView's
error/loading UI (use the same structure and classNames/markup used by
SoATreeView) so the function returns a valid JSX.Element and is
styled/structured like other states; update the conditional in App (the if
(!perspective?.uuid || !agent) branch) to return that JSX error node instead of
the raw string.
- Line 4: Replace the no-op TypeScript declaration import
'@coasys/flux-ui/dist/main.d.ts' in App.tsx with a runtime import of the
library's actual entry (either the JS module '@coasys/flux-ui' or the CSS/JS
asset like '@coasys/flux-ui/dist/main.css' / '@coasys/flux-ui/dist/main.js'),
removing the .d.ts import and importing the real runtime artifact so
styles/components are loaded at runtime; ensure any type-only imports remain as
TypeScript import type statements elsewhere if needed.

In `@views/soa-tree-view/src/components/SoANode.tsx`:
- Around line 106-115: The tags rendering in SoANode.tsx currently does
node.tags.split(',') which yields [''] for an empty string and produces an empty
pill; update the mapping to filter out empty or whitespace-only entries (e.g.,
after splitting call .map(t=>t.trim()).filter(t => t !== '') or
.filter(Boolean)) before rendering so only non-empty tags produce <span
key={...} className={styles.tag}> elements; ensure the key uses the trimmed tag
to avoid duplicate/empty keys.

In `@views/soa-tree-view/src/components/SoATreeView.tsx`:
- Around line 88-95: Parsed numeric fields node.confidence and node.priority can
become NaN when parseLiteralString yields non-numeric input; update the parsing
in SoATreeView.tsx so that after calling parseLiteralString you validate the
numeric conversion (use Number.parseFloat/Number.parseInt and check
Number.isFinite or isNaN) and apply a safe fallback (e.g., set confidence to
null or 0 and priority to a default integer) and optionally clamp confidence to
a 0–1 range; ensure you only assign the validated value to node.confidence and
node.priority to avoid propagating NaN into the UI.

In `@views/soa-tree-view/vite.config.ts`:
- Around line 7-15: The plugins array in vite.config.ts contains a stray comma
after the preact(...) entry which creates a sparse array; remove the extra comma
following the preact({ ... }) plugin so the array is contiguous (plugins: [
preact(...), cssInjectedByJsPlugin(), ]) and ensure only valid plugin entries
(preact and cssInjectedByJsPlugin) remain.

---

Nitpick comments:
In `@views/soa-tree-view/package.json`:
- Line 5: The package.json currently has an empty "author" field; update the
"author" value to include the package owner (e.g., a person name and optional
email or organization) by populating the "author" property in package.json (the
"author" key) with the appropriate string or object format so the package has
correct attribution.
- Line 12: The package.json currently contains a placeholder failing "test"
script; update the "test" script in package.json to a meaningful default (e.g.,
a no-op that exits 0 or a command that runs the chosen test runner) so CI won't
fail, and create a follow-up GitHub issue to add unit tests for the
tree-building logic (track functions/classes responsible for tree construction
such as the module that builds/parses the SOA tree). Ensure the issue references
the package.json "test" script and the specific tree-building module so adding
tests (e.g., for buildTree/parseTree) is tracked.

In `@views/soa-tree-view/src/App.tsx`:
- Line 5: Replace the internal-package import of AgentClient with the public
export: change the import "import { AgentClient } from
'@coasys/ad4m/lib/src/agent/AgentClient';" to import AgentClient from the
package entry point (same style as PerspectiveProxy), i.e., import AgentClient
from '@coasys/ad4m', so the file uses the public API surface instead of internal
paths and stays consistent with PerspectiveProxy.

In `@views/soa-tree-view/src/components/SoATreeView.tsx`:
- Around line 25-34: PROPERTY_PREDICATES in SoATreeView.tsx is defined but never
used; either remove the constant to eliminate dead code or keep it with an
explanatory comment or export if it's intended for future use. Locate the const
PROPERTY_PREDICATES declaration and either delete it or add a TODO comment
explaining its planned usage (or change it to an exported symbol if other
modules will reference it).
- Around line 144-147: The code in SoATreeView.tsx currently calls
perspective.queryLinks({}) and filters client-side for predicates starting with
"soa://"; change this to use server-side filtering via LinkQuery/perspective.get
to avoid fetching all links. Either iterate the PROPERTY_PREDICATES and
RELATIONSHIP_PREDICATES arrays and call perspective.get(new LinkQuery({
predicate: '<each-predicate>' })) to collect matching links, or (if supported)
use a LinkQuery with a predicate pattern/prefix for "soa://". Replace the
allLinks = await perspective.queryLinks({}) + client filter logic with the
server-side queries and merge their results into soaLinks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: baac8448-4149-477a-a76a-37ac5aec0a93

📥 Commits

Reviewing files that changed from the base of the PR and between 040a281 and ed37d16.

📒 Files selected for processing (11)
  • app/package.json
  • app/src/utils/fetchFluxApp.ts
  • views/soa-tree-view/package.json
  • views/soa-tree-view/src/App.module.css
  • views/soa-tree-view/src/App.tsx
  • views/soa-tree-view/src/components/SoANode.module.css
  • views/soa-tree-view/src/components/SoANode.tsx
  • views/soa-tree-view/src/components/SoATreeView.module.css
  • views/soa-tree-view/src/components/SoATreeView.tsx
  • views/soa-tree-view/src/main.ts
  • views/soa-tree-view/vite.config.ts

- Add author field to package.json
- Change test script to exit 0 (tests to be implemented in follow-up)
- Fix AgentClient import to use public export from @coasys/ad4m
- Remove unused PROPERTY_PREDICATES constant
- Add SoATree to ChannelView enum in flux-types
- Add plugin to viewOptions in constants/index.ts (visible in UI now)
- Add module declaration for TypeScript
- Fix CodeRabbit review comments:
  - Add author field in package.json
  - Fix test script to exit 0 instead of 1
  - Import AgentClient from public @coasys/ad4m export
  - Return JSX error element instead of raw string
  - Import main.css instead of .d.ts declaration
  - Remove unused PROPERTY_PREDICATES constant
  - Add NaN handling for confidence (clamped 0-1) and priority
  - Filter empty tags after split/trim
  - Remove stray comma in vite.config.ts plugins array
  - Add error style to App.module.css
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@views/soa-tree-view/src/components/SoANode.tsx`:
- Around line 45-52: The header currently uses hasDetails (which includes
node.status and node.priority) to show a caret and toggle details even when no
hidden content exists; update the expandability logic so hasDetails only
reflects fields that render inside the collapsible details (e.g.,
node.description, node.confidence, node.tags, node.source,
node.relationships.length > 0) and remove status/priority from that check. Then
change the clickable header element in SoANode from a plain div to a semantic
button and wire its aria-expanded to the component's expanded state; only render
the caret/toggle control when the new hasDetails is true so headers without
hidden content don’t show a disclosure affordance, and rely on the button for
keyboard accessibility instead of manual key handlers.

In `@views/soa-tree-view/src/components/SoATreeView.tsx`:
- Around line 41-61: buildTree currently only creates nodes when it sees a
'soa://title' link, causing items referenced only by relationships (e.g.,
'soa://rel_parent') to be omitted; update buildTree to register placeholder
SoANodeData entries in nodeMap for every unique source and target encountered in
soaLinks (use the existing nodeMap Map<string, SoANodeData> and the SoANodeData
shape) before or during processing, then when a 'soa://title' link is found
set/overwrite the title via parseLiteralString, leaving other fields as defaults
(modality 'observation', empty children/relationships) so the (untitled)
fallback in SoANode becomes reachable; ensure relationship handling
(parent/child link processing) uses these placeholders so rel_parent and other
predicates don't drop nodes.
- Around line 104-109: The parent-child assembly in SoATreeView.tsx currently
assumes an acyclic graph and can create cycles that cause infinite recursion in
SoANode render; update the loop over parentChildLinks to detect and reject
back-edges before pushing into parentNode.children (or alternatively record
ancestor paths during SoANode render). Concretely, for each { parent, child }
check that childNode is not an ancestor of parentNode (walk parent pointers or
use nodeMap to follow parent links) and skip/avoid adding the child if it would
create a cycle; also consider adding a boolean or Set (e.g., visitedPaths) to
SoANode render as a defensive check to stop recursion if a cycle is encountered.
Ensure checks reference parentChildLinks, nodeMap, parentNode.children, childSet
and the SoANode render/build path so cycles are rejected or short-circuited.
- Around line 84-86: The parser currently assigns any finite integer to
node.priority for pred 'soa://priority', which can produce out-of-range badges;
after parsing the integer (from parseLiteralString(target)), validate it's a
finite integer and clamp it into the expected 1–5 range before setting
node.priority (e.g., if Number.isFinite(prio) use Math.max(1, Math.min(5,
prio)), otherwise set node.priority = undefined). Update the assignment in the
'soa://priority' branch to apply this clamp and preserve undefined for invalid
parses.
- Line 137: The code calls perspective.queryLinks({}) but queryLinks is not on
the public PerspectiveProxy API; replace that call with the public API pattern
used elsewhere: construct a LinkQuery and call perspective.get(new
LinkQuery(...)) instead (mirror the approach in SubjectRepository where
perspective.get(new LinkQuery(...)) is used). Update the call site that
currently references perspective.queryLinks to use LinkQuery and
perspective.get, ensuring imports/reference to LinkQuery and the
PerspectiveProxy get method are used rather than the lower-level
PerspectiveClient API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e2de1a4e-84ab-4321-9ba0-62e28b5efa7c

📥 Commits

Reviewing files that changed from the base of the PR and between ed37d16 and 1c0bb64.

📒 Files selected for processing (9)
  • app/src/constants/index.ts
  • app/src/modules.ts
  • packages/types/src/index.ts
  • views/soa-tree-view/package.json
  • views/soa-tree-view/src/App.module.css
  • views/soa-tree-view/src/App.tsx
  • views/soa-tree-view/src/components/SoANode.tsx
  • views/soa-tree-view/src/components/SoATreeView.tsx
  • views/soa-tree-view/vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • views/soa-tree-view/package.json
  • views/soa-tree-view/src/App.tsx
  • views/soa-tree-view/src/App.module.css
  • views/soa-tree-view/vite.config.ts

Comment on lines +45 to +52
const hasDetails =
node.description ||
node.confidence != null ||
node.status ||
node.tags ||
node.priority != null ||
node.source ||
node.relationships.length > 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use a real disclosure control only when there is hidden content.

hasDetails currently counts status and priority, but those values only render in the header. Nodes with only those fields still show a caret and toggle an empty details block. On top of that, the clickable header is a plain div, so keyboard users cannot expand or collapse it. Compute expandability from the fields that actually render below and switch the header to a button with aria-expanded.

🔧 Suggested direction
+  const tags = node.tags?.split(',').map((tag) => tag.trim()).filter(Boolean) ?? [];
   const hasDetails =
-    node.description ||
+    !!node.description ||
     node.confidence != null ||
-    node.status ||
-    node.tags ||
-    node.priority != null ||
-    node.source ||
+    tags.length > 0 ||
+    !!node.source ||
     node.relationships.length > 0;
+  const isExpandable = hasChildren || hasDetails;
 
-      <div
+      <button
+        type="button"
         className={`${styles.nodeHeader} ${expanded ? styles.expanded : ''}`}
-        onClick={() => setExpanded(!expanded)}
+        onClick={() => isExpandable && setExpanded((value) => !value)}
+        aria-expanded={isExpandable ? expanded : undefined}
       >
         <span className={styles.toggle}>
-          {hasChildren || hasDetails ? (expanded ? '\u25BE' : '\u25B8') : '\u00A0\u00A0'}
+          {isExpandable ? (expanded ? '\u25BE' : '\u25B8') : '\u00A0\u00A0'}
         </span>
         ...
-      </div>
+      </button>
 
-          {node.tags && (
+          {tags.length > 0 && (
             <div className={styles.property}>
               <span className={styles.propLabel}>Tags</span>
               <div className={styles.tagList}>
-                {node.tags.split(',').map((tag) => tag.trim()).filter(Boolean).map((tag) => (
+                {tags.map((tag) => (
                   <span key={tag} className={styles.tag}>{tag}</span>
                 ))}
               </div>
             </div>
           )}

Also applies to: 58-64, 87-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoANode.tsx` around lines 45 - 52, The
header currently uses hasDetails (which includes node.status and node.priority)
to show a caret and toggle details even when no hidden content exists; update
the expandability logic so hasDetails only reflects fields that render inside
the collapsible details (e.g., node.description, node.confidence, node.tags,
node.source, node.relationships.length > 0) and remove status/priority from that
check. Then change the clickable header element in SoANode from a plain div to a
semantic button and wire its aria-expanded to the component's expanded state;
only render the caret/toggle control when the new hasDetails is true so headers
without hidden content don’t show a disclosure affordance, and rely on the
button for keyboard accessibility instead of manual key handlers.

Comment on lines +41 to +61
function buildTree(soaLinks: LinkExpression[]): SoANodeData[] {
const nodeMap = new Map<string, SoANodeData>();
const parentChildLinks: { parent: string; child: string }[] = [];

// First pass: find all nodes (anything with a soa://title link)
for (const link of soaLinks) {
const pred = link.data.predicate;
const base = link.data.source;

if (pred === 'soa://title') {
if (!nodeMap.has(base)) {
nodeMap.set(base, {
base,
title: '',
modality: 'observation',
children: [],
relationships: [],
});
}
nodeMap.get(base)!.title = parseLiteralString(link.data.target);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't require soa://title to discover a node.

buildTree() only seeds nodeMap from title links, so any SoA item that is only referenced by soa://rel_parent or another relationship disappears entirely. That also makes the (untitled) fallback in SoANode unreachable. Register placeholder nodes for every SoA source/target you encounter, then fill in the title when it exists.

🔧 Suggested direction
 function buildTree(soaLinks: LinkExpression[]): SoANodeData[] {
   const nodeMap = new Map<string, SoANodeData>();
   const parentChildLinks: { parent: string; child: string }[] = [];
+  const ensureNode = (base: string) => {
+    if (!nodeMap.has(base)) {
+      nodeMap.set(base, {
+        base,
+        title: '',
+        modality: 'observation',
+        children: [],
+        relationships: [],
+      });
+    }
+    return nodeMap.get(base)!;
+  };
 
-  // First pass: find all nodes (anything with a soa://title link)
+  // First pass: register every SoA node we touch
   for (const link of soaLinks) {
     const pred = link.data.predicate;
     const base = link.data.source;
+    const target = link.data.target;
+    ensureNode(base);
+    if (pred === 'soa://rel_parent' || RELATIONSHIP_PREDICATES.includes(pred)) {
+      ensureNode(target);
+    }
 
     if (pred === 'soa://title') {
-      if (!nodeMap.has(base)) {
-        nodeMap.set(base, {
-          base,
-          title: '',
-          modality: 'observation',
-          children: [],
-          relationships: [],
-        });
-      }
-      nodeMap.get(base)!.title = parseLiteralString(link.data.target);
+      ensureNode(base).title = parseLiteralString(target);
     }
   }

Also applies to: 89-98

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` around lines 41 - 61,
buildTree currently only creates nodes when it sees a 'soa://title' link,
causing items referenced only by relationships (e.g., 'soa://rel_parent') to be
omitted; update buildTree to register placeholder SoANodeData entries in nodeMap
for every unique source and target encountered in soaLinks (use the existing
nodeMap Map<string, SoANodeData> and the SoANodeData shape) before or during
processing, then when a 'soa://title' link is found set/overwrite the title via
parseLiteralString, leaving other fields as defaults (modality 'observation',
empty children/relationships) so the (untitled) fallback in SoANode becomes
reachable; ensure relationship handling (parent/child link processing) uses
these placeholders so rel_parent and other predicates don't drop nodes.

Comment on lines +84 to +86
} else if (pred === 'soa://priority') {
const prio = parseInt(parseLiteralString(target), 10);
node.priority = Number.isFinite(prio) ? prio : undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp priority before rendering the P-scale.

The UI exposes a bounded P1-P5 badge, but this parser accepts any finite integer. Invalid data will surface as P0 or P99 instead of the expected range.

🔧 Proposed fix
     } else if (pred === 'soa://priority') {
       const prio = parseInt(parseLiteralString(target), 10);
-      node.priority = Number.isFinite(prio) ? prio : undefined;
+      node.priority = Number.isFinite(prio) ? Math.max(1, Math.min(5, prio)) : undefined;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` around lines 84 - 86, The
parser currently assigns any finite integer to node.priority for pred
'soa://priority', which can produce out-of-range badges; after parsing the
integer (from parseLiteralString(target)), validate it's a finite integer and
clamp it into the expected 1–5 range before setting node.priority (e.g., if
Number.isFinite(prio) use Math.max(1, Math.min(5, prio)), otherwise set
node.priority = undefined). Update the assignment in the 'soa://priority' branch
to apply this clamp and preserve undefined for invalid parses.

Comment on lines +104 to +109
for (const { parent, child } of parentChildLinks) {
const parentNode = nodeMap.get(parent);
const childNode = nodeMap.get(child);
if (parentNode && childNode) {
parentNode.children.push(childNode);
childSet.add(child);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard soa://rel_parent against cycles.

This builder assumes the parent graph is acyclic. A back-edge like R -> A, A -> B, B -> A will make the recursive SoANode render walk forever once it reaches A. Reject back-edges while assembling children, or track a visited path during render/build.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` around lines 104 - 109,
The parent-child assembly in SoATreeView.tsx currently assumes an acyclic graph
and can create cycles that cause infinite recursion in SoANode render; update
the loop over parentChildLinks to detect and reject back-edges before pushing
into parentNode.children (or alternatively record ancestor paths during SoANode
render). Concretely, for each { parent, child } check that childNode is not an
ancestor of parentNode (walk parent pointers or use nodeMap to follow parent
links) and skip/avoid adding the child if it would create a cycle; also consider
adding a boolean or Set (e.g., visitedPaths) to SoANode render as a defensive
check to stop recursion if a cycle is encountered. Ensure checks reference
parentChildLinks, nodeMap, parentNode.children, childSet and the SoANode
render/build path so cycles are rejected or short-circuited.

setLoading(true);
// TODO: Optimize with server-side filtering by querying each soa:// predicate individually
// instead of fetching all links and filtering client-side
const allLinks = await perspective.queryLinks({});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In @coasys/ad4m v0.11.1, what is the public PerspectiveProxy API for fetching links? Does it include queryLinks(), or should callers use get(new LinkQuery(...))?

💡 Result:

In @coasys/ad4m v0.11.1, the public PerspectiveProxy API for fetching links is perspective.get(query: LinkQuery) (and convenience helpers like getSingleTarget). It does not expose queryLinks() on PerspectiveProxy. (docs.ad4m.dev)

queryLinks(uuid, query) exists on the lower-level client (PerspectiveClient / ad4mClient.perspective) and is what PerspectiveProxy.get() delegates to internally. (coasys.github.io)

So callers should use:

await perspective.get(new LinkQuery({ source: "...", predicate: "...", target: "..." }))

(or an object typed as LinkQuery, depending on your TS setup). (docs.ad4m.dev)

Citations:


queryLinks() is not part of the public PerspectiveProxy API.

In @coasys/ad4m v0.11.1, the code calls perspective.queryLinks({}), but queryLinks() exists only on the lower-level PerspectiveClient, not on PerspectiveProxy. Use perspective.get(new LinkQuery(...)) instead, matching the pattern in packages/api/src/factory/SubjectRepository.ts (Lines 106-108), or the view will fail on first load.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/components/SoATreeView.tsx` at line 137, The code
calls perspective.queryLinks({}) but queryLinks is not on the public
PerspectiveProxy API; replace that call with the public API pattern used
elsewhere: construct a LinkQuery and call perspective.get(new LinkQuery(...))
instead (mirror the approach in SubjectRepository where perspective.get(new
LinkQuery(...)) is used). Update the call site that currently references
perspective.queryLinks to use LinkQuery and perspective.get, ensuring
imports/reference to LinkQuery and the PerspectiveProxy get method are used
rather than the lower-level PerspectiveClient API.

Address CodeRabbit review feedback:
- Import AgentClient as type from public @coasys/ad4m export
- Remove internal package path import
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@views/soa-tree-view/src/App.tsx`:
- Around line 7-15: The Props type and App component currently include an unused
agent and the render guard checks agent; remove the unnecessary agent usage by
either (A) deleting agent from the Props type and the App parameter list and
removing the agent check from the early-return condition (leave only
perspective?.uuid and source checks), or (B) if you intend to reserve agent for
later, mark it optional (agent?: AgentClient) in Props and remove the agent
check from the if condition so rendering depends only on perspective/source;
update the App signature and any references to match the chosen approach
(identify the Props type, App function, and the early-return conditional to
change).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2bc4ecf8-169e-47b2-9fcd-6f00817d91e7

📥 Commits

Reviewing files that changed from the base of the PR and between 1c0bb64 and 78626a6.

📒 Files selected for processing (1)
  • views/soa-tree-view/src/App.tsx

Comment on lines +7 to +15
type Props = {
agent: AgentClient;
perspective: PerspectiveProxy;
source: string;
};

export default function App({ agent, perspective, source }: Props) {
if (!perspective?.uuid || !agent) {
return (
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect SoATreeView's props/implementation and all `agent` references in this package.
sed -n '1,220p' views/soa-tree-view/src/components/SoATreeView.tsx
rg -n -C2 --glob 'views/soa-tree-view/src/**' '\bagent\b'

Repository: coasys/flux

Length of output: 7127


Remove the unnecessary agent guard, or make it optional if agent may be required later.

SoATreeView declares only perspective and source in its Props type and never references agent. The guard at line 14 (if (!perspective?.uuid || !agent)) blocks rendering even though agent is not used. For a read-only tree view that depends solely on perspective and source, either remove the agent check or make it optional (e.g., agent?: AgentClient) if agent is reserved for future use by child components.

Also applies to: lines 8, 18

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@views/soa-tree-view/src/App.tsx` around lines 7 - 15, The Props type and App
component currently include an unused agent and the render guard checks agent;
remove the unnecessary agent usage by either (A) deleting agent from the Props
type and the App parameter list and removing the agent check from the
early-return condition (leave only perspective?.uuid and source checks), or (B)
if you intend to reserve agent for later, mark it optional (agent?: AgentClient)
in Props and remove the agent check from the if condition so rendering depends
only on perspective/source; update the App signature and any references to match
the chosen approach (identify the Props type, App function, and the early-return
conditional to change).

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.

2 participants