Skip to content

Flux deploy preview for use in WE#596

Open
jhweir wants to merge 78 commits into
devfrom
feat/sparql-cleanup-ad4m-version-test-4
Open

Flux deploy preview for use in WE#596
jhweir wants to merge 78 commits into
devfrom
feat/sparql-cleanup-ad4m-version-test-4

Conversation

@jhweir
Copy link
Copy Markdown
Contributor

@jhweir jhweir commented May 9, 2026

Deploy preview for use in WE

feat/sparql-1.2-cleanup + ad4m version bump to 0.13.0-test-4 + explicit through: 'ad4m://has_child' on HasMany relations

Summary by CodeRabbit

  • New Features

    • CI now runs lint/typecheck; build process tightened. Live SPARQL subscriptions replace many global listeners for more responsive updates. Lightweight summary models added for faster sidebar/list rendering.
  • Bug Fixes

    • Fixed async race conditions with load-sequence guards. Normalized channel/community IDs and improved membership checks. More robust notification and error handling.
  • Tests

    • Migrated to Vitest and added extensive unit/integration coverage for core flows.
  • Chores

    • Updated dependency pins and workspace override handling; added a typecheck task.

Add Channel.recentConversations() and Channel.pinnedConversations() static
methods that use single SPARQL queries instead of iterative channel.get()
loops.

- Channel.recentConversations(perspective, limit): single query with
  ORDER BY DESC(?lastActivity) LIMIT — replaces N×M×K iterative walk
- Channel.pinnedConversations(perspective): single query for pinned
  channels with their conversation IDs
- Both methods deduplicate by channelId and handle errors gracefully

Tests prove single SPARQL call per invocation (not N+1).
…dels

- ChannelSummary: Channel without @hasmany relations — no hidden graph
  exploration during sidebar/list hydration
- MessageSummary: Message without SPARQL getter properties (replyingTo,
  isPopular) — retains simple @hasmany (reactions, thread, replies)
- Both exported from @coasys/flux-api barrel

Tests verify no relation/getter queries fire on lightweight models.
- useCommunityService: use ChannelSummary instead of Channel for allChannels
  live query — eliminates @hasmany relation hydration on every link change
- Replace N+1 getPinnedConversations with Channel.pinnedConversations()
- Replace N×M×K getRecentConversations with Channel.recentConversations()
- Replace iterative getChannelsWithConversations with Conversation.findOne()
- Update handleParticipantTracking for ChannelSummary (no participants prop)
- MessageList.tsx: add lazy evaluateGetters for visible messages (replyingTo)
  — works with WS-2 deepQuery inversion where getters are skipped by default

Tests verify scoped queries and no iterative patterns remain.
- Remove perspective.addListener('link-added', handleLinkAdded) from
  TimelineColumn.vue — was firing on EVERY link in the perspective
- Remove handleLinkAdded, getDataFull, getDataIncremental, debounce state
- Replace with useLiveQuery(Conversation, perspective, { parent: channel })
  — only fires when Conversation instances under this channel change
- Watch conversationInstances for reactive refreshAllData() calls
- Import useLiveQuery, remove onUnmounted (handled by useLiveQuery)

Tests verify no raw listeners, no old handler functions, and scoped
useLiveQuery usage.
All WS-3/4/5/6 tests now strip // and /* */ comments before pattern
matching, preventing false failures from comment text like
'No @hasmany relations' or 'replaces perspective.addListener'.

All 72 tests pass.
Code comments now describe what the code does rather than
referencing internal planning workstream identifiers.
Renamed test files for clarity.
The previous query applied LIMIT before deduplication, so a busy
channel with many items could consume all LIMIT rows, starving other
channels from the result set.

Switch to GROUP BY ?channelId with SAMPLE/MAX aggregation so the
LIMIT applies after per-channel grouping.  Oxigraph supports full
SPARQL 1.1 aggregation (already used elsewhere in this file via
COUNT(DISTINCT ...)), so remove the misleading comment about GROUP BY
availability.

The client-side seen Map is kept as a safety net but should no longer
be needed.
Rapid watch(conversationInstances, ...) triggers could fire
refreshAllData() concurrently.  Without a guard, a slow earlier call
could resolve after a newer one, overwriting fresher state.

Add an in-flight promise guard with a pending flag: concurrent calls
coalesce onto the running promise, and if a call arrived while
in-flight, one additional refresh runs after the current one
completes.
The getPinnedConversations and getRecentConversations tests silently
passed when the regex failed to match (the if-block was skipped).

- Add expect(match).not.toBeNull() so a regex miss is a real failure
- Apply comment-stripping in both tests for consistency
…subscriptions

- Remove refreshAllData(), getConversations(), getUnprocessedItems(),
  refreshInFlight, refreshPending, and refreshTrigger from TimelineColumn
- Replace with reactive watchEffect that maps conversationInstances
  directly from the existing useLiveQuery subscription
- Add separate watchEffect for unprocessed items, triggered by
  conversationInstances changes
- Extract sidebar refresh into a dedicated watch on conversation name
- Extract AI task check into a dedicated watch on unprocessedItems
- Remove onMounted — reactive watchEffect handles initial load
- Remove refreshTrigger prop from TimelineBlock (and recursive children)
- TimelineBlock now watches props.data instead of refreshTrigger
- Add structural tests verifying removal of imperative patterns
- All 20 structural tests pass, pnpm build succeeds
… proper unit tests

- Remove jest, ts-jest, @types/jest from packages/api
- Add vitest with config (globals, node environment)
- Delete regex/structural test files that loaded source as strings:
  - community-service-scoping.test.ts
  - timeline-column-scoping.test.ts
  - channel-sparql-methods.test.ts
- Rewrite ChannelSummary.test.ts: uses getModelMetadata() to verify
  no relations, expected properties, and flag metadata
- Rewrite MessageSummary.test.ts: uses getModelMetadata() to verify
  no getter properties, expected relations, and flag metadata
- Rewrite channel-query.test.ts: tests set-difference logic and
  three-query SPARQL integration pattern with mocked perspectives
- Add channel.test.ts: tests Channel.recentConversations() and
  Channel.pinnedConversations() with mocked perspectives —
  verifies SPARQL query structure, result mapping, deduplication,
  error handling, and limit parameter
- Existing parseLit.test.ts works unchanged (Vitest globals)

All 38 tests pass across 5 test files.
The build script temporarily sets file: overrides for local linking.
These should not be committed.
…ives

- Channel.recentConversations/pinnedConversations SPARQL queries used
  string literal 'true' to match channel_is_conversation/channel_is_pinned,
  but AD4M stores these as literal:json URIs. Use ad4m://fn/parse_literal
  SPARQL function (consistent with buildSPARQLWhereFilters).

- markRaw(perspective) in useCommunityService to prevent Vue reactive Proxy
  from wrapping PerspectiveProxy, which breaks TypeScript #private fields
  (WeakMap lookup fails when 'this' is a Proxy instead of raw instance).

- Add { immediate: true } to sidebar watchers so data loads on first render
  instead of waiting for a change event that may never fire.

- Null-safe signallingService for imported perspectives without a
  neighbourhood (LinkLanguageFailedToInstall).

- Private perspective fallback: try private://UUID when neighbourhood://
  URL lookup fails in createCommunityService.

- Temporarily skip Conversation.findOne cache population — hangs on
  perspectives without a link language. Sidebar still renders with
  channelId + lastActivity timestamps.
…terns

- Convert all GRAPH ?g { s p o } patterns to direct default-graph triples
- Use rdf:reifies <<( s p o )>> for link metadata (author, timestamp)
- Updated files: channel/index.ts, conversation/index.ts,
  conversation-subgroup/index.ts, registerMobileNotifications.ts
- Add PREFIX rdf: where reifier patterns are used
- Companion to ad4m/core Oxigraph 0.5.7 upgrade
- restoreNeighbourhoodPrefix now passes through URLs that already contain ://
- Router guard matches perspectives by raw UUID after stripping private:// prefix
…mplates

- SidebarList: use item.channel?.id with channelId fallback for v-for key
- SidebarItem: guard c.channel before accessing .id in nested channel check
- CommunityView: optional chain on channelData.channel in channel card grid
- aiStore: replace non-null assertions with optional chains on channel.id
- routeUtils: guard restoreNeighbourhoodPrefix against undefined input
…on ordering

Channel.allItems() and Channel.unprocessedItems() now query
flux://transcript_started_at and use it (when present) instead of the
link-creation timestamp. This ensures transcription messages sort by
when speech occurred rather than when Whisper processing completed.

Matches the existing pattern in ConversationSubgroup.itemsData().

Also consolidates channel test files into a single channel.test.ts.
HexaField and others added 20 commits May 7, 2026 21:18
…tibility

Root cause: ad4m/package.json uses 'overrides.core: { hoist: false }' format
which pnpm v10 rejects (requires string format). Netlify likely upgraded
to pnpm v10, breaking feat/sparql-1.2-cleanup branch builds.

Solution: Skip AD4M linking for deploy previews (pull/*/head), use
published npm packages instead. This matches how origin/dev works.
- Remove broken pipe issue (| tail -5 caused SIGPIPE exit code 1)
- Simplify deploy preview logic: check for matching AD4M branch first
- Pin pnpm to v9.15.0 for AD4M builds (v10 incompatible with object-format workspace overrides)
- Add @coasys/hooks-helpers override when building AD4M from source
- Restore package.json to dev baseline for deploy previews without matching AD4M branches
- Build now completes successfully for feat/sparql-1.2-cleanup branch

Fixes: Netlify build failures preventing deploy preview validation checks
- Always set all 5 @coasys package overrides when building AD4M from source
- Removes unnecessary file existence check (if AD4M cloned, files exist)
- Now matches the overrides in build.yaml and tests.yaml exactly:
  * @coasys/ad4m → file:./ad4m/core
  * @coasys/ad4m-connect → file:./ad4m/connect
  * @coasys/hooks-helpers → file:./ad4m/ad4m-hooks/helpers
  * @coasys/ad4m-react-hooks → file:./ad4m/ad4m-hooks/react
  * @coasys/ad4m-vue-hooks → file:./ad4m/ad4m-hooks/vue

Ensures consistency between Netlify deploy script and GitHub Actions CI.
@netlify
Copy link
Copy Markdown

netlify Bot commented May 9, 2026

Deploy Preview for fluxsocial-dev ready!

Name Link
🔨 Latest commit 7a7b525
🔍 Latest deploy log https://app.netlify.com/projects/fluxsocial-dev/deploys/6a037e0bea3a8700081588bc
😎 Deploy Preview https://deploy-preview-596--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
Copy Markdown

coderabbitai Bot commented May 9, 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: a64fab60-5b27-4ba5-9bb3-ea6f12a7e327

📥 Commits

Reviewing files that changed from the base of the PR and between b524573 and 7a7b525.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Replaces infer/prolog and global link listeners with SPARQL queries and targeted subscribeQuery subscriptions; adds ChannelSummary/MessageSummary models; rewrites Channel/Conversation/Subgroup queries; migrates tests to Vitest; updates CI to add lint/typecheck and local AD4M/hooks pnpm overrides; refactors services, stores, and many UI components to use new APIs and safe async/subscription patterns.

Changes

Complete refactor to SPARQL-driven reactive architecture

Layer / File(s) Summary
CI & build scripts
.github/workflows/*, scripts/build-with-ad4m-link.sh, turbo.json, package.json
Adds local AD4M/hooks pnpm overrides, pins pnpm v9.15.0 for AD4M builds, and runs pnpm lint, pnpm build, and informational pnpm typecheck in CI; adds turbo typecheck task.
API Summary Models & exports
packages/api/src/channel/ChannelSummary.ts, packages/api/src/message/MessageSummary.ts, packages/api/src/index.ts
Adds lightweight ChannelSummary and MessageSummary models with decorated fields and re-exports them from the API index; adds model metadata tests.
Channel queries & helpers
packages/api/src/channel/index.ts, packages/api/src/channel/*test.ts
Rewrites allItems(), unprocessedItems(), and totalItemCount() to use direct ad4m://has_child + reifier metadata and adds recentConversations() and pinnedConversations() static SPARQL helpers; updates relation wiring to explicit through links.
Conversation & Subgroup queries
packages/api/src/conversation/*, packages/api/src/conversation-subgroup/*
Migrate stats/topics/subgroupsData/itemsData to direct SPARQL (remove GRAPH wrappers), use rdf:reifies for timestamp resolution and VALUES batching, and adjust relations to explicit through wiring.
Semantic/embeddings & topics
packages/api/src/semantic-relationship/*, packages/api/src/topic/*
Refactors embedding and topic retrieval from perspective.infer to perspective.querySparql, adds TYPE_MAP and uses parseLit for literal parsing.
Tests & test infra
packages/api/jest.config.cjs (removed), packages/api/package.json, packages/api/vitest.config.ts, many tests
Moves to Vitest (config + scripts) and adds large Vitest suites for Channel and Conversation behaviors and metadata tests.
Timeline subscriptions & tests
app/src/components/conversation/timeline/*, app/src/components/conversation/timeline/__tests__/*
Switches TimelineColumn/TimelineBlock to useLiveQuery + channel-scoped subscribeQuery; removes refreshTrigger; debounced unprocessed-item refresh and unit tests for subscription lifecycle and AI trigger logic.
Community service & stores
app/src/composables/useCommunityService.ts, app/src/stores/*
Community service uses ChannelSummary and markRaw perspectives; batched conversation hydration via new Channel helpers; appStore uses shallowRef; aiStore queues ChannelSummary and instantiates full Channel for processing.
View & component refactors
app/src/views/*, views/*
Many components switch from proxy/infer flows to explicit AD4M APIs (getInstanceClasses/getSubjectData/Link), NewClass switched to SHACL, Task.pl removed in favor of TaskModel, async sequence guards added, subscriptions replace global listeners.
Helpers, packages, small fixes
packages/*, app/src/utils/*, ESLint
Fix lit import paths, deprecate subscribeToLinks, remove perspective-subscribe helpers, change usePerspectives return shape, strip/restore route utils handle private://, ESLint ignore/preset tweaks, mobile notification SPARQL tweak, createCommunity timestamp change, and minor const/type fixes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • coasys/flux#580: Both PRs perform the same SPARQL migration work—e.g., both modify packages/api/src/channel/index.ts (allItems/unprocessedItems → querySparql), conversation/subgroup SPARQL rewrites and literal parsing helpers—so they are directly related.
  • coasys/flux#549: The main PR implements parts of the AD4M model migration (new lightweight Channel/Message summaries, id-based APIs, SPARQL/query refactors, useLiveQuery changes, and related workflow updates) that directly overlap the migration work described in PR #549.
  • coasys/flux#577: Main PR extends and tweaks the AD4M branch-linking CI/build flow (workflow, build script, and AD4M package overrides) originally introduced in PR #577, so the changes are directly related.

Suggested reviewers

  • lucksus

Poem

Hop hop, I weave new threads of light,
From Prolog dusk to SPARQL bright—
Subscriptions hum, summaries sing,
CI checks and typechecks spring.
A rabbit hops, the tests take wing. 🥕✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sparql-cleanup-ad4m-version-test-4

@jhweir jhweir marked this pull request as ready for review May 9, 2026 19:49
@jhweir jhweir changed the title Draft: Deploy preview for use in WE Flux deploy preview for use in WE May 9, 2026
Copy link
Copy Markdown

@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: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
views/kanban-view/src/components/Board/Board.tsx (2)

186-188: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard setTasks against an undefined entries.

useLiveQuery's data can be undefined before the first result arrives (or after a query error). Calling setTasks(undefined) then makes transformData's tasks.forEach(...) (Line 112) throw Cannot read properties of undefined (reading 'forEach'). The state is initialized as [] (Line 41), so preserve that contract.

🛡️ Suggested fix
   useEffect(() => {
-    setTasks(entries);
+    setTasks(entries ?? []);
   }, [JSON.stringify(entries), perspective.uuid]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/kanban-view/src/components/Board/Board.tsx` around lines 186 - 188, The
useEffect that calls setTasks(entries) should guard against entries being
undefined (useLiveQuery can return undefined); update the effect in Board.tsx so
it only calls setTasks when entries !== undefined (or falls back to an empty
array) to preserve the state contract initialized as [] and avoid
transformData's tasks.forEach throwing; reference the useEffect that sets tasks,
the entries variable from useLiveQuery, and the setTasks state updater when
making the change.

168-180: ⚠️ Potential issue | 🔴 Critical | 💤 Low value

Use correct method name subjectClasses() instead of listRegisteredClasses().

The code calls listRegisteredClasses() which doesn't exist in @coasys/ad4m 0.11.1. The correct API is subjectClasses(), which returns Promise<string[]> containing plain class names like 'Task'.

Additionally, since ensureSDNASubjectClass() is idempotent (it checks for existing classes before registering), the outer if guard is redundant and can be removed. This also eliminates the issue where classes remains unpopulated when the condition is false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/kanban-view/src/components/Board/Board.tsx` around lines 168 - 180, The
effect currently calls a non-existent method listRegisteredClasses() and
conditionally registers the Task class which leaves classes unset when the guard
prevents registration; update the effect to call perspective.subjectClasses()
(which returns Promise<string[]>) or, per the comment, simply remove the outer
if and always call perspective.ensureSDNASubjectClass(Task). After
ensureSDNASubjectClass(Task) resolves, call
getClasses(perspective).then(setClasses) so classes are populated; keep the rest
of the effect (including checkNeighbourhoodAuthor() and the dependency on
perspective.uuid) unchanged and reference useEffect, checkNeighbourhoodAuthor,
perspective.subjectClasses(), perspective.ensureSDNASubjectClass(Task),
getClasses, and setClasses when making the change.
views/poll-view/src/components/PollCard/PollCard.tsx (1)

110-112: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix useEffect dependency to trigger when votes change.

The current dependency answers.map((a) => a.id).join(',') only changes when answer IDs change (added/removed/reordered). It does NOT trigger when votes within existing answers change.

Problem flow:

  1. User votes → vote() persists the change and calls buildAnswerData() at line 107 with potentially stale subscription data
  2. Subscription updates with fresh votes → component re-renders
  3. useEffect sees identical answer IDs → doesn't run → processedAnswers remains stale
  4. UI shows incorrect vote counts despite subscription having fresh data

This breaks vote count updates after voting.

🔧 Proposed fix
   useEffect(() => {
     buildAnswerData();
-  }, [answers.map((a) => a.id).join(',')]);
+  }, [answers]);

This will correctly trigger whenever the answers array updates (including vote changes), ensuring vote counts stay in sync with the subscription data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/poll-view/src/components/PollCard/PollCard.tsx` around lines 110 - 112,
The useEffect dependency currently only watches answer IDs so processedAnswers
isn't rebuilt when vote counts change; update the useEffect dependency to watch
the entire answers array (e.g., change the dependency from answers.map(a =>
a.id).join(',') to simply answers) so that useEffect(() => { buildAnswerData();
}, [answers]) re-runs whenever answers (including votes) update; ensure this
touches the useEffect surrounding buildAnswerData and references the answers and
processedAnswers variables so vote-driven subscription updates correctly refresh
the UI.
views/table-view/src/components/Header/Header.tsx (1)

35-77: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

onUpdate (and properties) are unreferenced in the rendered JSX — likely leftover from the refactor.

The new onUpdate callback (Lines 35–53) and the properties computed at Line 56 are not wired to anything in the returned JSX: the component now only renders the header title, the Current Entry label, and the expand button. None of them reach DisplayValue or any other element that calls onUpdate. Either the JSX is missing the property list / editable display that was intended to use this update path, or this code is dead and should be removed.

Also, entry is initialized via useState({}), so if (entry) is always truthy and the final return <span>{source}</span>; (Line 82) is unreachable.

Same async-staleness caveat applies as in Entry.tsx/History.tsx: a cancelled guard in the effect and after the update's refresh would prevent stale setEntry calls when source changes mid-flight.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/Header/Header.tsx` around lines 35 - 77, The
onUpdate function and the properties variable are dead code after the refactor:
onUpdate (async function onUpdate) and properties (computed from entry) are
never used in the returned JSX, and entry is initialized as useState({}) making
the `if (entry)` check always truthy so the fallback return is unreachable. Fix
by either reintroducing the intended property list / editable DisplayValue
components that consume properties and call onUpdate (e.g., render DisplayValue
rows with onclick/update handlers) or remove onUpdate and properties entirely if
editing was intentionally removed; also change entry initialization to null
(useState(null)) and add a cancelled guard in the effect and after the async
refresh (where setEntry is called, e.g., after perspective.getSubjectData and in
onUpdate) to avoid stale setState when source changes mid-flight. Ensure you
update or remove references to setEntry/getSubjectData/getClassShape to keep
semantics consistent.
views/table-view/src/components/TableView/TableView.tsx (1)

250-259: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Change predicate to 'ad4m://has_child' to match the codebase convention.

The PR objective calls for explicit through: 'ad4m://has_child' for HasMany relations. Throughout the codebase, all parent–child links use the fully-qualified 'ad4m://has_child' predicate. Here createEntry uses a bare 'has_child' string, which is inconsistent with every other link creation in the repo and with the SHACL/HasMany relation definitions. Entries created here will not query correctly against the standardized predicate.

Diff
   await perspective.add({
     source: source || 'ad4m://self',
-    predicate: 'has_child',
+    predicate: 'ad4m://has_child',
     target: uuid,
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/TableView/TableView.tsx` around lines 250 -
259, The createEntry function is using the bare predicate 'has_child' when
adding the parent→child link; update the perspective.add call inside createEntry
to use the fully-qualified predicate 'ad4m://has_child' (matching the repo
convention and SHACL/HasMany relation definitions) so entries will be queryable;
ensure the call in createEntry({ perspective, subjectClass, source }) replaces
predicate: 'has_child' with predicate: 'ad4m://has_child' (no other logic
changes).
views/table-view/src/components/History/History.tsx (1)

41-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Verify the new AD4M API contract and fix race condition in async state updates.

  1. API verification: perspective.getInstanceClasses(source) and perspective.getSubjectData(className, source) are both available in @coasys/ad4m 0.13.0. They return Promise<string[]> and Promise<T> respectively, matching the code's assumptions.

  2. Race condition—requires fixing: If source or perspective.uuid changes faster than getSubjectData resolves, an older async response can overwrite newer state, displaying the wrong entry. No cancellation guard exists. This pattern is repeated identically in Header.tsx and Entry.tsx.

Implement a cancellation flag or AbortController in the effect cleanup:

♻️ Suggested staleness guard
   useEffect(() => {
-    fetchSourceClasses(source);
+    let cancelled = false;
+    (async () => {
+      const classResults = await perspective.getInstanceClasses(source);
+      if (cancelled) return;
+      if (classResults?.length > 0) {
+        const className = classResults[0];
+        const data = await perspective.getSubjectData(className, source);
+        if (cancelled) return;
+        setClasses(classResults);
+        setEntry({ id: source, ...data });
+      } else {
+        setClasses([]);
+        setEntry({ id: source });
+      }
+    })();
+    return () => { cancelled = true; };
   }, [source, perspective.uuid]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/History/History.tsx` around lines 41 - 57,
The effect calling fetchSourceClasses (which calls
perspective.getInstanceClasses and perspective.getSubjectData) can suffer a race
where an earlier async response overwrites newer state; update the
useEffect/fetchSourceClasses pattern to use a staleness guard (e.g., a local
cancelled flag or AbortController) that is set in the effect cleanup and checked
before calling setClasses and setEntry, and apply the same fix to the identical
patterns in Header.tsx and Entry.tsx; keep the same API calls but ensure you
only set state when the request is not cancelled and the current
perspective.uuid/source still match.
app/src/composables/useCommunityService.ts (2)

41-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Type contract violation when signallingService is null.

The interface declares agentsInChannel: AgentData[] and agentsInCall: AgentData[] as non-optional arrays, but with signallingService?.getAgentsInChannel(...)?.value, both fields are undefined when signallingService is null (the new private:// perspective path). Consumers iterating these (e.g., sidebar/agent badges) will hit undefined.length/undefined.map.

Either widen the type to AgentData[] | undefined and update consumers, or provide a stable [] fallback here.

🛡️ Proposed fix
   pinnedConversationsWithAgents = computed((): ChannelDataWithAgents[] => {
     return pinnedConversations.value.map((data) => ({
       ...data,
       channel: allChannels.value.find((c) => c.id === data.channelId),
       conversation: data.conversationId ? conversationCache.get(data.conversationId) : undefined,
-      agentsInChannel: signallingService?.getAgentsInChannel(data.channelId).value,
-      agentsInCall: signallingService?.getAgentsInCall(data.channelId).value,
+      agentsInChannel: signallingService?.getAgentsInChannel(data.channelId).value ?? [],
+      agentsInCall: signallingService?.getAgentsInCall(data.channelId).value ?? [],
       children: undefined,
     }));
   });

(Apply the same ?? [] fallback at lines 182-183, 192-193, and 199-200.)

Also applies to: 167-204

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/composables/useCommunityService.ts` around lines 41 - 50, The
ChannelDataWithAgents interface currently requires agentsInChannel and
agentsInCall but the code that builds ChannelDataWithAgents uses
signallingService?.getAgentsInChannel(...)?.value (and similar calls) which
yields undefined when signallingService is null; update the builder to provide a
stable empty-array fallback (use the nullish-coalescing fallback like ?? []) for
each place that assigns agentsInChannel and agentsInCall (e.g., the expressions
using signallingService?.getAgentsInChannel(...).value and
signallingService?.getAgentsInCall(...).value) so the constructed
ChannelDataWithAgents always has AgentData[] values, or alternatively change the
interface fields to AgentData[] | undefined and update consumers—prefer adding
the ?? [] fallback to keep the interface unchanged.

476-512: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Sync state listener registered but never removed in cleanup().

perspective.addSyncStateChangeListener(syncStateListener) (line 482) is added during service creation, but the cleanup() function (lines 509-512) only removes the link-added listener and does not unsubscribe from sync state changes. Even though cleanup() is called on component unmount, the sync state callback will continue to fire against a stale isSynced ref, causing memory leaks and ref mutations.

The proposed fix assumes removeSyncStateChangeListener() exists, but a codebase search found no such API. Verify with the @coasys/ad4m library documentation (v0.11.1) whether this removal method is available, or consider alternative cleanup approaches if the API does not exist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/composables/useCommunityService.ts` around lines 476 - 512, The sync
state listener added via
perspective.addSyncStateChangeListener(syncStateListener) is never removed in
cleanup(), causing stale updates to isSynced; locate the syncStateListener
registration and either call the corresponding removal API (e.g.,
perspective.removeSyncStateChangeListener(syncStateListener) or an unsubscribe
function returned by addSyncStateChangeListener) inside cleanup(), or—if the
`@coasys/ad4m` v0.11.1 API lacks a removal method—replace the raw listener with a
guarded handler that becomes a no-op on teardown (e.g., set a local "mounted"
flag false in cleanup() and have syncStateListener return immediately when
mounted is false) so that isSynced is not mutated after cleanup. Ensure changes
touch the syncStateListener, addSyncStateChangeListener call, and cleanup() so
the listener is properly unsubscribed or neutralized.
scripts/build-with-ad4m-link.sh (1)

42-44: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

A && B || C swallows build failures.

The pattern [ -f X ] && (cd ... && pnpm exec tsc) || echo "Skipping ..." is not equivalent to if/then/else: when the file exists but pnpm exec tsc fails, the || branch runs and the script proceeds as if hooks built successfully. Combined with set -e, this also unexpectedly suppresses the set -e propagation inside the parentheses. If hooks fail to compile, the deploy preview will silently link against stale/missing builds.

🛡️ Proposed fix using explicit `if`
-  [ -f ad4m-hooks/helpers/tsconfig.json ] && (cd ad4m-hooks/helpers && pnpm exec tsc) || echo "Skipping ad4m-hooks/helpers"
-  [ -f ad4m-hooks/react/tsconfig.json ] && (cd ad4m-hooks/react && pnpm exec tsc) || echo "Skipping ad4m-hooks/react"
-  [ -f ad4m-hooks/vue/tsconfig.json ] && (cd ad4m-hooks/vue && pnpm exec tsc) || echo "Skipping ad4m-hooks/vue"
+  if [ -f ad4m-hooks/helpers/tsconfig.json ]; then (cd ad4m-hooks/helpers && pnpm exec tsc); else echo "Skipping ad4m-hooks/helpers"; fi
+  if [ -f ad4m-hooks/react/tsconfig.json ]; then (cd ad4m-hooks/react && pnpm exec tsc); else echo "Skipping ad4m-hooks/react"; fi
+  if [ -f ad4m-hooks/vue/tsconfig.json ]; then (cd ad4m-hooks/vue && pnpm exec tsc); else echo "Skipping ad4m-hooks/vue"; fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/build-with-ad4m-link.sh` around lines 42 - 44, The three one-liners
using the pattern "[ -f X ] && (cd ... && pnpm exec tsc) || echo 'Skipping ...'"
incorrectly treat a failing "pnpm exec tsc" as success and run the echo branch;
replace each with an explicit if block that checks -f, then runs the subshell
"cd ... && pnpm exec tsc" and exits non-zero (or re-throw) if tsc fails, else
echo the "Skipping ad4m-hooks/..." message; ensure you do not wrap the tsc call
in a construct that swallows errors (avoid the "A && B || C" pattern) so that
set -e will propagate failures from the subshell.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/package.json`:
- Line 19: Confirm that the "typecheck" script which uses vue-tsc@^3.2.7 is
actually compatible with the resolved TypeScript version in pnpm-lock.yaml:
inspect pnpm-lock.yaml for the resolved vue-tsc and typescript versions (vue-tsc
should be 3.2.7 range and typescript should satisfy >=5.0.0, e.g., ^5.0.4), run
the typecheck script locally to reproduce any issues, and if you find a mismatch
or known minor-version edge case pin or bump the devDependency (typescript or
vue-tsc) in package.json to a compatible version or add an explicit
devDependency entry to avoid an incompatible resolved version.

In `@app/src/components/conversation/timeline/TimelineColumn.vue`:
- Around line 203-219: The TimelineColumn component currently sets up a
long-lived SPARQL subscription using the captured channelUrl inside an IIFE (see
the subscribeQuery block and channelItemsSub), which won’t reset when Vue Router
reuses the component for a different channel; fix by either adding a route-based
key on the parent (e.g., set :key="route.params.channelId" where Conversation
renders TimelineColumn) or by adding a watcher inside TimelineColumn for
route.params.channelId that disposes channelItemsSub, clears any related
watchers (AI/sidebar watchers, unprocessed items logic), and re-runs the
subscription setup (the IIFE logic) when the channel changes; also sanitize or
assert channelUrl before interpolating into the SPARQL string (or use
restoreChannelPrefix consistently) to avoid injecting malformed IRIs into
subscribeQuery.

In `@app/src/views/main/modals/CreateCommunityModal.vue`:
- Around line 210-211: The key used to check membership is built incorrectly:
change the key derivation used by perspectiveIsCommunity to match the
myCommunities store format (which uses perspective.sharedUrl ||
`private://${perspective.uuid}` in appStore.getMyCommunities()). Update the code
that defines key (used by perspectiveIsCommunity) to use perspective.sharedUrl
|| `private://${perspective.uuid}` so perspectives without a sharedUrl will
correctly match entries in myCommunities.value.

In `@packages/api/src/channel/index.ts`:
- Around line 167-168: The interpolation of unprocessedIds into valuesClause
(`unprocessedIds.map(id => `<${id}>`)`) is unsafe; validate or escape each id
before building the SPARQL VALUES clause (e.g., reject or log and skip ids
containing '>', whitespace, or other IRI-breaking characters) to avoid
constructing a malformed query that causes unprocessedItems() to fail silently;
update the code that produces valuesClause to validate each id against a safe
pattern (or explicitly escape/URN-encode it), and ensure failures are handled
deterministically (throw or return a clear error/log) rather than falling back
to an empty array.

In `@packages/api/src/conversation/conversation.test.ts`:
- Around line 15-17: Update the stale comment above the vi.mock('@coasys/ad4m',
...) block: either change the hardcoded version string
`@coasys/ad4m@0.13.0-test-2` to `0.13.0-test-4` or replace it with a generalized
note like "published 0.13.0 test releases" and add a short TODO to verify
exports; then check whether the current published package actually exports
Model/HasMany and if so remove the mock layer in the vi.mock('@coasys/ad4m',
...) setup to avoid unnecessary no-op decorators.

In `@packages/flux-container/package.json`:
- Around line 22-23: The package.json for `@coasys/flux-container` currently pins
transitive dependencies "@coasys/ad4m" and "@coasys/ad4m-connect" to the
prerelease "0.13.0-test-4"; create a tracking issue to restore these to the
stable GA tag (e.g., "0.13.0" or the eventual release tag) before publishing a
non-preview release of flux-container, and update the package.json entries for
"@coasys/ad4m" and "@coasys/ad4m-connect" to use the stable version (or a semver
range) at release time to avoid leaking test prerelease pins to downstream
consumers.

In `@scripts/build-with-ad4m-link.sh`:
- Line 5: The BRANCH assignment can fail under set -u if none of BRANCH, HEAD,
GITHUB_HEAD_REF, or GITHUB_REF are set; update the assignment so each fallback
uses a safe default before the # operator and the overall default is an empty
string. Specifically, change the BRANCH assignment that references BRANCH, HEAD,
GITHUB_HEAD_REF and the parameter expansion on GITHUB_REF so each symbol is
expanded with a safe default (e.g., use the ":-" or "-" style fallback for HEAD,
GITHUB_HEAD_REF and GITHUB_REF before applying the `#refs/heads/` substring
operation) and ensure the final BRANCH default is empty to avoid unbound
variable errors when running with set -u.

In `@views/graph-view/src/components/CommunityGraph.tsx`:
- Around line 105-122: The effect can leak subscriptions because
sourceSub/targetSub are assigned only inside subscribeQuery.then callbacks; add
a local boolean flag (e.g., cancelled = false) at the top of the useEffect and
in the cleanup set cancelled = true and dispose any already-assigned
subscriptions, and in each subscribeQuery.then handler check the flag: if
cancelled then immediately dispose the newly-arrived subscription
(sub.dispose()) and return; otherwise assign to sourceSub/targetSub and wire
sub.onResult(() => refresh()). Also remove the unnecessary optional chaining on
perspective (or early-return if perspective is falsy) so you're not calling
perspective?.subscribeQuery with a non-nullable PerspectiveProxy.

In `@views/kanban-view/src/components/Entry/Entry.tsx`:
- Around line 15-19: The useEffect calling
perspective.getNamedOptions(selectedClass) can resolve after selectedClass
changes or component unmounts, causing stale setNamedOptions calls and it also
lacks error handling; update the effect to create a cancellation flag (e.g., let
cancelled = false) and in the cleanup set cancelled = true, then after the
promise resolves only call setNamedOptions if not cancelled (and optionally
clear named options on cancel), and add a .catch handler to the promise to
handle/log errors instead of letting rejections be silent; reference the
existing useEffect, perspective.getNamedOptions, setNamedOptions, selectedClass
and perspective.uuid when applying this change.

In `@views/kanban-view/src/hooks/useAssociations.tsx`:
- Around line 33-43: The effect in useAssociations builds a SPARQL query using
predicate but the dependency array only tracks perspective.uuid and source, so
changes to predicate won't resubscribe; update the effect's dependency array to
include predicate (i.e., [perspective.uuid, source, predicate]) so
perspective.subscribeQuery(spql) is re-run when predicate changes and the
previous subscription (sub) is disposed and replaced correctly.
- Around line 35-42: The async subscription created via
perspective.subscribeQuery must be guarded so we don't register callbacks or
leak handles after unmount: when calling perspective.subscribeQuery(sparql)
capture the returned promise and track a local "cancelled" (or "mounted") flag;
after the promise resolves, check the flag before assigning sub or calling
handle.onResult(() => fetchLinks()); if cancelled, call handle.dispose()
immediately instead of attaching listeners; also add a .catch(...) to the
promise to log/handle subscription errors. Ensure the cleanup function sets the
cancelled flag and disposes sub if already assigned (sub?.dispose()).

In `@views/poll-view/src/components/PollCard/PollCard.tsx`:
- Around line 31-34: The project is using the new include query syntax with
useLiveQuery(Answer, perspective, { parent: { model: Poll, id: poll.id }, query:
{ include: { votes: true } } }) but views/poll-view/package.json is pinned to
`@coasys/ad4m-react-hooks` v0.11.1 which lacks that syntax; update
views/poll-view/package.json to use `@coasys/ad4m-react-hooks` version
0.13.0-test-4 so the include option works, and modify the render code that
currently calls Vote.findAll() (referenced as Vote.findAll) to instead use the
eager-loaded votes from the subscription (access votes on the Answer objects
returned by useLiveQuery, e.g., answers -> answer.votes) so you reuse the loaded
data rather than refetching.

In `@views/table-view/src/components/NewClass/NewClass.tsx`:
- Around line 40-44: In addClass(), validate and sanitize inputs before calling
buildSHACLShape/perspective.addShacl: ensure the class name (variable name) is
non-empty and URL/IRI-safe (no spaces, '#' or '/'; escape or reject invalid
chars), ensure every column in columns (added via addNewColumn) has non-empty
name and predicate, and enforce uniqueness of column predicates (reject or mark
duplicates), then surface a clear validation error to the user (do not call
perspective.addShacl or onSaved until validation passes); run these checks
immediately in addClass and only call buildSHACLShape(passedName, passedColumns,
perspective) and perspective.addShacl when all validations succeed.
- Around line 232-233: The code builds an IRI using the raw user-provided name
(const ns = `recipe://${name}`; new SHACLShape(`${ns}Shape`, `${ns}#${name}`))
which can produce invalid IRIs; sanitize/percent-encode the name before
embedding it (or validate/restrict to a safe char class) and use the encoded
value when constructing ns and the shape IRI (i.e., apply the same encoding
helper to name for both the namespace and the `${ns}#${name}` target so both
IRIs are valid).
- Around line 227-276: The SHACL property datatype is using a non-standard URI
'xsd://string' inside buildSHACLShape; change the propShape.datatype assignment
to use the CURIE 'xsd:string' (or full XSD URI) so the SHACL parser accepts it;
update the line that sets datatype in the propShape object (where propShape is
constructed in buildSHACLShape) to 'xsd:string', keep the rest of the propShape
construction, addProperty calls, and constructorActions logic unchanged, and
then re-run tests to confirm parser accepts the shape.

In `@views/table-view/src/components/TableView/TableView.tsx`:
- Around line 61-66: When perspective.listRegisteredClasses() yields an empty
array the component sets selected to '' but the header "+" button still renders
and can call createEntry({ subjectClass: '' }); update the header button
rendering logic in the TableView component to guard against no selection: check
classes.length and selected and either disable the "+" button when !selected or
hide the button entirely when classes.length === 0 so createEntry is never
invoked with an empty subjectClass; locate the UI that renders the "+" button
(references: selected, classes, createEntry, createSubject) and add the
conditional rendering/disabled-state there.

---

Outside diff comments:
In `@app/src/composables/useCommunityService.ts`:
- Around line 41-50: The ChannelDataWithAgents interface currently requires
agentsInChannel and agentsInCall but the code that builds ChannelDataWithAgents
uses signallingService?.getAgentsInChannel(...)?.value (and similar calls) which
yields undefined when signallingService is null; update the builder to provide a
stable empty-array fallback (use the nullish-coalescing fallback like ?? []) for
each place that assigns agentsInChannel and agentsInCall (e.g., the expressions
using signallingService?.getAgentsInChannel(...).value and
signallingService?.getAgentsInCall(...).value) so the constructed
ChannelDataWithAgents always has AgentData[] values, or alternatively change the
interface fields to AgentData[] | undefined and update consumers—prefer adding
the ?? [] fallback to keep the interface unchanged.
- Around line 476-512: The sync state listener added via
perspective.addSyncStateChangeListener(syncStateListener) is never removed in
cleanup(), causing stale updates to isSynced; locate the syncStateListener
registration and either call the corresponding removal API (e.g.,
perspective.removeSyncStateChangeListener(syncStateListener) or an unsubscribe
function returned by addSyncStateChangeListener) inside cleanup(), or—if the
`@coasys/ad4m` v0.11.1 API lacks a removal method—replace the raw listener with a
guarded handler that becomes a no-op on teardown (e.g., set a local "mounted"
flag false in cleanup() and have syncStateListener return immediately when
mounted is false) so that isSynced is not mutated after cleanup. Ensure changes
touch the syncStateListener, addSyncStateChangeListener call, and cleanup() so
the listener is properly unsubscribed or neutralized.

In `@scripts/build-with-ad4m-link.sh`:
- Around line 42-44: The three one-liners using the pattern "[ -f X ] && (cd ...
&& pnpm exec tsc) || echo 'Skipping ...'" incorrectly treat a failing "pnpm exec
tsc" as success and run the echo branch; replace each with an explicit if block
that checks -f, then runs the subshell "cd ... && pnpm exec tsc" and exits
non-zero (or re-throw) if tsc fails, else echo the "Skipping ad4m-hooks/..."
message; ensure you do not wrap the tsc call in a construct that swallows errors
(avoid the "A && B || C" pattern) so that set -e will propagate failures from
the subshell.

In `@views/kanban-view/src/components/Board/Board.tsx`:
- Around line 186-188: The useEffect that calls setTasks(entries) should guard
against entries being undefined (useLiveQuery can return undefined); update the
effect in Board.tsx so it only calls setTasks when entries !== undefined (or
falls back to an empty array) to preserve the state contract initialized as []
and avoid transformData's tasks.forEach throwing; reference the useEffect that
sets tasks, the entries variable from useLiveQuery, and the setTasks state
updater when making the change.
- Around line 168-180: The effect currently calls a non-existent method
listRegisteredClasses() and conditionally registers the Task class which leaves
classes unset when the guard prevents registration; update the effect to call
perspective.subjectClasses() (which returns Promise<string[]>) or, per the
comment, simply remove the outer if and always call
perspective.ensureSDNASubjectClass(Task). After ensureSDNASubjectClass(Task)
resolves, call getClasses(perspective).then(setClasses) so classes are
populated; keep the rest of the effect (including checkNeighbourhoodAuthor() and
the dependency on perspective.uuid) unchanged and reference useEffect,
checkNeighbourhoodAuthor, perspective.subjectClasses(),
perspective.ensureSDNASubjectClass(Task), getClasses, and setClasses when making
the change.

In `@views/poll-view/src/components/PollCard/PollCard.tsx`:
- Around line 110-112: The useEffect dependency currently only watches answer
IDs so processedAnswers isn't rebuilt when vote counts change; update the
useEffect dependency to watch the entire answers array (e.g., change the
dependency from answers.map(a => a.id).join(',') to simply answers) so that
useEffect(() => { buildAnswerData(); }, [answers]) re-runs whenever answers
(including votes) update; ensure this touches the useEffect surrounding
buildAnswerData and references the answers and processedAnswers variables so
vote-driven subscription updates correctly refresh the UI.

In `@views/table-view/src/components/Header/Header.tsx`:
- Around line 35-77: The onUpdate function and the properties variable are dead
code after the refactor: onUpdate (async function onUpdate) and properties
(computed from entry) are never used in the returned JSX, and entry is
initialized as useState({}) making the `if (entry)` check always truthy so the
fallback return is unreachable. Fix by either reintroducing the intended
property list / editable DisplayValue components that consume properties and
call onUpdate (e.g., render DisplayValue rows with onclick/update handlers) or
remove onUpdate and properties entirely if editing was intentionally removed;
also change entry initialization to null (useState(null)) and add a cancelled
guard in the effect and after the async refresh (where setEntry is called, e.g.,
after perspective.getSubjectData and in onUpdate) to avoid stale setState when
source changes mid-flight. Ensure you update or remove references to
setEntry/getSubjectData/getClassShape to keep semantics consistent.

In `@views/table-view/src/components/History/History.tsx`:
- Around line 41-57: The effect calling fetchSourceClasses (which calls
perspective.getInstanceClasses and perspective.getSubjectData) can suffer a race
where an earlier async response overwrites newer state; update the
useEffect/fetchSourceClasses pattern to use a staleness guard (e.g., a local
cancelled flag or AbortController) that is set in the effect cleanup and checked
before calling setClasses and setEntry, and apply the same fix to the identical
patterns in Header.tsx and Entry.tsx; keep the same API calls but ensure you
only set state when the request is not cancelled and the current
perspective.uuid/source still match.

In `@views/table-view/src/components/TableView/TableView.tsx`:
- Around line 250-259: The createEntry function is using the bare predicate
'has_child' when adding the parent→child link; update the perspective.add call
inside createEntry to use the fully-qualified predicate 'ad4m://has_child'
(matching the repo convention and SHACL/HasMany relation definitions) so entries
will be queryable; ensure the call in createEntry({ perspective, subjectClass,
source }) replaces predicate: 'has_child' with predicate: 'ad4m://has_child' (no
other logic changes).

---

Nitpick comments:
In `@app/.eslintrc.js`:
- Line 6: The project removed the Prettier preset from the ESLint extends array
(see the extends entry in .eslintrc.js) but left Prettier-related packages
installed; update package.json by removing `@vue/eslint-config-prettier`,
eslint-config-prettier, and eslint-plugin-prettier from
dependencies/devDependencies so they no longer install, and run pnpm install to
update the lockfile; ensure no other config files reference these plugins
(search for "prettier" and the specific package names) before committing.

In `@app/package.json`:
- Around line 18-21: The package.json "test" script still invokes Jest ("test":
"jest --env=jsdom"); update it to use Vitest to match the migration in
packages/api by replacing the script value with a Vitest invocation (e.g.,
"test": "vitest" and include the appropriate environment flag such as
--environment=jsdom if browser-like DOM is needed) and then open a follow-up
task to remove Jest-related devDependencies (`@types/jest`, jest, ts-jest,
babel-jest, jest-transform-stub) once all app tests are converted; target the
"test" script entry in package.json when making this change.

In
`@app/src/components/conversation/timeline/__tests__/timeline-subscription.test.ts`:
- Around line 83-189: Switch the tests to fake timers to make debounce waits
deterministic: call vi.useFakeTimers() before running debounce-related code
(e.g., in the describe's beforeEach or at the start of individual it blocks),
replace all new Promise(r => setTimeout(r, N)) waits with await
vi.advanceTimersByTimeAsync(N) (or await
vi.runAllTimersAsync()/advanceTimersByTimeAsync where appropriate), and restore
real timers with vi.useRealTimers() in afterEach; update tests referencing
createDebouncedRefreshHandler and handler.scheduleRefresh() to use these
fake-timer helpers so the debounce window is driven deterministically.

In `@app/src/components/conversation/timeline/TimelineColumn.vue`:
- Around line 355-362: The comment above the watchEffect is stale (refers to the
removed "link-added listener") and the watchEffect uses a non-obvious reactive
dependency hack; update the comment to mention that unprocessed items are
re-fetched when the SPARQL subscribeQuery block triggers conversation updates
and simplify the reactive registration by replacing the watchEffect(async () =>
{ const _ = conversationInstances.value; await refreshUnprocessedItems(); })
with an explicit watch on conversationInstances that calls
refreshUnprocessedItems (i.e., use watch(conversationInstances,
refreshUnprocessedItems, { immediate: true }) or equivalent) and remove the
outdated "link-added listener" text; keep references to subscribeQuery,
conversationInstances, watchEffect/watch, and refreshUnprocessedItems to find
the code to modify.

In `@app/src/composables/useCommunityService.ts`:
- Around line 524-530: The watcher on recentConversations needs to trigger when
AI becomes enabled after load; update the watch so it either (A) uses {
immediate: true } on recentConversations or (B) watches both recentConversations
and aiEnabled together (e.g., watch([recentConversations, aiEnabled], ...)) so
that when aiEnabled flips to true the handler runs; inside the handler keep the
existing guard using processingStateChecked.value and call
aiStore.findProcessingTasksInCommunity(perspective.sharedUrl || '') after the
HEARTBEAT_INTERVAL as before to preserve behavior.

In `@app/src/router/index.ts`:
- Around line 96-97: The membership check is redundant: remove the separate
p.uuid === communityId test and only compare against the normalized id (rawId)
that strips the "private://" prefix; update the isMember expression (which uses
appStore.myPerspectives, neighbourhoodUrl, communityId, rawId) to use p.uuid ===
rawId (and keep the neighbourhoodUrl check) so a single normalized comparison
covers both prefixed and unprefixed communityId cases.

In `@app/src/stores/aiStore.ts`:
- Around line 259-263: The code incorrectly casts
processingQueue.value[0].channel to Channel using toRaw(channel) as Channel
despite the ProcessingQueueItem typing the field as ChannelSummary; stop lying
to the type system and either treat it as ChannelSummary or just read the id
directly. Replace the cast with a ChannelSummary-typed value (or avoid toRaw
entirely) and use that to access rawChannel.id when calling
communityServiceStore.getCommunityService(...).getConversation(rawChannel.id!)
and getParentChannel(rawChannel.id!), ensuring the variable is declared as
ChannelSummary (or simply const id = processingQueue.value[0].channel.id) so no
Channel-only properties are assumed.
- Line 273: The verbose console.log in aiStore.ts (the line with console.log('🤖
Checking for unprocessed items in channel:', rawChannel.id)) spams logs on every
dequeued task; change it to a lower-severity debug call or gate it behind a
runtime/dev flag. Locate the log inside the AI processing function in the
aiStore (where rawChannel is used) and either replace console.log with
console.debug or wrap the console call in a check (e.g., if
(process.env.DEBUG_AI || isDev) ...) so the message only appears when debugging
is enabled. Ensure the new approach uses the same message and rawChannel.id for
context.

In `@app/src/stores/appStore.ts`:
- Line 105: Document the fallback community ID convention used when computing
key (const key = perspective.sharedUrl || `private://${perspective.uuid}`): add
a JSDoc comment near the key derivation (and/or extract a named constant like
PRIVATE_COMMUNITY_PREFIX or make a helper getCommunityKey(perspective)) that
explains the "private://{uuid}" format and intended semantics, then update
usages (references to perspective.sharedUrl, perspective.uuid, and
router/CreateCommunityModal usage) to reference the constant/helper so the
convention is explicit and consistent across the codebase.

In `@app/src/utils/routeUtils.ts`:
- Around line 7-21: restoreNeighbourhoodPrefix uses communityId.includes('://')
which can false-positive on embedded strings; change the check in
restoreNeighbourhoodPrefix to only treat a leading scheme as already-prefixed
(e.g. test communityId against a scheme-at-start regex like
/^[a-z][a-z0-9+\-.]*:\/\// or use startsWith checks for known schemes) so that
only true protocol-prefixed IDs are returned unchanged; also ensure
stripNeighbourhoodPrefix (and any callers) keep handling both 'neighbourhood://'
and 'private://' as before.

In `@app/src/views/main/community/channel/modals/ChangeChannelModal.vue`:
- Around line 39-58: The watcher currently observes only channel, but its
callback also depends on perspective (used in the guard and to construct new
Channel), so change the watch to observe both channel and perspective (e.g.,
watch([channel, perspective], ...)) so the callback re-runs when perspective
becomes available; keep the existing logic using channelLoadSeq, the
Channel(perspective, newChannel.id) construction, fullChannel.get({ views: true
}), and assignments to views.value and error handling intact, and preserve the {
immediate: true } option.

In `@app/src/views/main/community/channel/modals/ManageChannelPluginsModal.vue`:
- Around line 125-149: The watcher currently only watches channel but also reads
perspective, so when perspective initializes later the handler won't run; update
the watch call that wraps channel (the watch(...) block) to watch both channel
and perspective (e.g., watch([channel, perspective], ...)) and adjust the
handler to accept the new source tuple (or read current values inside) so the
guard using perspective and the logic that constructs new Channel, calls
fullChannel.get, and updates views, selectedPlugins and viewsLoadSeq behaves
correctly; ensure seq checks and the existing try/catch logic remain intact.

In `@app/src/views/main/community/sidebar/SidebarItem.vue`:
- Line 131: The callback for item.children in the inNestedChannel computation
currently uses (c: any); replace the any with the proper ChannelDataWithAgents
type (or remove explicit typing so TypeScript infers it from item.children) so
that the predicate becomes typed correctly; update the reference in the
inNestedChannel expression (which calls stripChannelPrefix(c.channel.id) ===
currentChannelId) to use (c: ChannelDataWithAgents) to restore type-safety for
item.children and its channel property.

In `@packages/api/package.json`:
- Around line 8-33: The package.json currently uses a broad vitest range
("devDependencies" entry "vitest": "^2"); narrow this to a specific minor to
reduce cross-machine drift (e.g., change the "vitest" devDependency to "^2.1" or
another chosen minor), keeping the existing scripts ("test" and "test:watch")
and config intact; update the "devDependencies" vitest entry accordingly so the
workspace consistently installs the pinned minor version.

In `@packages/api/src/channel/channel.test.ts`:
- Around line 298-322: Update the misleading block comment above the helper
runUnprocessedItems used in the Channel.unprocessedItems() test to reflect that
it only simulates the set-difference portion (two-query portion) rather than the
full three-query pattern; locate the comment immediately above the
runUnprocessedItems function and replace "the three-query pattern from
Channel.unprocessedItems()" with wording like "the set-difference portion of the
three-query pattern from Channel.unprocessedItems()" so the test's intent and
the assertion that querySparql is called twice are unambiguous.

In `@packages/api/src/channel/index.ts`:
- Around line 278-282: The OPTIONAL reifier block allows ?_parent to match any
previous parent, causing multiple ?chanCreatedTs per channel; restrict it by
scoping ?_parent to the current/community parent or collapse multiple timestamps
inside the subquery. Update the OPTIONAL block around ?_chanReifier rdf:reifies
<<( ?_parent <flux://has_channel> ?channelId )>> to either add a FILTER that
ties ?_parent to the known community parent variable used elsewhere or replace
the unconstrained timestamp selection with an aggregation (e.g., SELECT ...
(MAX(?chanCreatedTs) AS ?chanCreatedTs) or SAMPLE-like isolation) or otherwise
limit to a single chosen ?chanCreatedTs (LIMIT 1) so the outer COALESCE and
final MAX(?ts) no longer receive duplicated rows.

In `@packages/api/src/conversation/conversation.test.ts`:
- Around line 622-624: The test uses vi.restoreAllMocks() in the beforeEach
which removes hoisted vi.mock implementations and forces manual re-mocking of
ensureLLMTasks, LLMTaskWithExpectedOutputs, and getProfile; replace
vi.restoreAllMocks() with vi.clearAllMocks() (or remove the global restore and
scope overrides to individual it blocks) in the beforeEach so hoisted module
mocks remain intact and only call history is reset, ensuring the existing mocked
implementations for ensureLLMTasks, LLMTaskWithExpectedOutputs, and getProfile
are preserved and future tests don't silently break.
- Around line 411-447: The tests currently swallow errors from
Conversation.processNewExpressions which can mask regressions; update each test
in the processNewExpressions describe block (e.g., "creates a batch...", "calls
updateProcessingState...", etc.) to either fully mock the external dependencies
so processNewExpressions resolves (mock the LLM/task client returned by
createMockClient, any async methods used inside processNewExpressions such as
detectNewGroup/updateGroupTopics/addLinks/save/get) or remove the empty
try/catch and assert the failure explicitly with await
expect(conv.processNewExpressions(...)).rejects.toThrow(/specific reason/) so
the thrown error becomes part of the test contract; ensure you reference the
Conversation.processNewExpressions invocation and updateState assertions
(updateState({ step: ... })) when adjusting each test.

In `@packages/api/src/message/MessageSummary.ts`:
- Around line 28-35: Add an isInstance filter to the `@HasMany` decorators in
MessageSummary for the self-referential relations so they only traverse Message
entities: update the decorators on reactions, thread, and replies (the `@HasMany`
entries in MessageSummary) to include isInstance: 'Message' (use the string
literal form for self-references) to ensure only Message-type targets are
returned.

In `@packages/api/src/semantic-relationship/index.ts`:
- Around line 166-173: The plural-to-singular logic in allItemEmbeddingsByType
is brittle — replace the naive itemType.slice(0, -1) approach by normalizing and
validating the incoming type against known keys: check TYPE_MAP for the provided
itemType first (accepting both plural and singular forms), then try a
deterministic conversion (e.g., strip trailing "s" only if present) or use an
explicit lookup map from common plural forms to singulars before falling back to
error; update references to the local variable singular and the TYPE_MAP lookup
so the function robustly handles inputs like "Messages", "Message", or
unexpected casing and logs a clear error if no mapping exists.

In `@scripts/build-with-ad4m-link.sh`:
- Around line 29-30: The script currently suppresses pnpm install errors with
`npm i -g pnpm@9.15.0 2>/dev/null || true`, which can leave an incompatible pnpm
on PATH; after attempting to install/update pnpm, run `pnpm --version` and
validate it matches 9.x (or explicitly equals 9.15.0) and if not, print a clear
error message and exit non‑zero before continuing to the `pnpm install` step so
the failure is visible and the build doesn't proceed with pnpm v10; update the
shell block containing the install command to include this verification and a
failing exit when the version check fails.

In `@turbo.json`:
- Around line 27-30: The typecheck pipeline entry ("typecheck") currently omits
an explicit "inputs" array which causes Turbo to hash the entire package
contents and over-invalidate; update the "typecheck" task to add an "inputs"
field (alongside the existing "dependsOn": ["^build"] and "outputs": []) and
restrict it to only relevant files such as "src/**/*.{ts,tsx,vue}",
"tsconfig*.json", and any shared type-related configs (e.g.,
"packages/**/tsconfig*.json") so cache keys are stable and changes to unrelated
files (like README.md) don’t bust the cache.

In `@views/graph-view/src/components/CommunityGraph.tsx`:
- Around line 88-100: In CommunityGraph's useEffect, avoid calling fetchSnapShot
twice by removing the initial fetchSnapShot(perspective, source) call and
instead invoke the existing refresh() function once at the start; keep refresh()
as the single place that calls fetchSnapShot(perspective, source) and updates
state via setNodes and setLinks (and consolidate any error handling/logging
there), and drop the stray console.log to keep behavior DRY and maintain one
error path.

In `@views/kanban-view/src/components/Board/Board.tsx`:
- Around line 150-161: getClasses currently performs N sequential
perspective.getNamedOptions calls causing N+1 round trips; change it to fetch
options in parallel by mapping allClasses to an array of getNamedOptions
promises, await Promise.all, then filter those results for non-empty option
objects and return the corresponding class names. Update the getClasses function
(which calls perspective.listRegisteredClasses and perspective.getNamedOptions)
to use Promise.all and preserve the existing return type so callers (e.g., the
selectedClass effect) receive the same string[] result.
- Around line 190-193: Current useEffect mixes class discovery and column
loading causing unnecessary re-runs of getClasses when selectedClass changes and
a race with ensureSDNASubjectClass; split into two effects: one effect whose
dependency is [perspective.uuid] only to call
getClasses(perspective).then(setClasses) (move discovery out), and a separate
effect that depends on [perspective.uuid, selectedClass] to call loadColumns()
and perform the auto-selection logic; remove the duplicate
getClasses(perspective).then(setClasses) from the init/ensureSDNASubjectClass
flow and instead either await/chain ensureSDNASubjectClass to complete before
running the discovery effect or trigger a manual re-discovery (call getClasses
and setClasses) after ensureSDNASubjectClass finishes so setClasses ordering is
deterministic.

In `@views/table-view/src/components/Entry/Entry.tsx`:
- Around line 16-49: The async flows in fetchSourceClasses and onUpdate can
write stale state: add a cancellation guard (e.g., a local "cancelled" boolean
or AbortController token created inside the useEffect and passed into
fetchSourceClasses/onUpdate) so that after any await you check the token before
calling setClasses or setEntry (and clean up by toggling cancelled in the effect
cleanup). Also fix the dead render branch by initializing entry to null
(useState(null)) and guarding rendering on entry (or remove the unreachable
return) so the component only renders the fallback when entry is actually null;
update references to entry, setEntry, fetchSourceClasses, onUpdate and the
useEffect accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9dcf9676-ddd4-403e-9102-2c75ff9a607e

📥 Commits

Reviewing files that changed from the base of the PR and between dd19522 and aecca96.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (73)
  • .github/workflows/build.yaml
  • .github/workflows/tests.yaml
  • app/.eslintignore
  • app/.eslintrc.js
  • app/package.json
  • app/src/__tests__/audio-processor.test.ts
  • app/src/components/conversation/timeline/TimelineBlock.vue
  • app/src/components/conversation/timeline/TimelineColumn.vue
  • app/src/components/conversation/timeline/__tests__/timeline-subscription.test.ts
  • app/src/composables/useCommunityService.ts
  • app/src/composables/useSignallingService.ts
  • app/src/containers/Conversation.vue
  • app/src/containers/Conversations.vue
  • app/src/router/index.ts
  • app/src/stores/aiStore.ts
  • app/src/stores/appStore.ts
  • app/src/utils/registerMobileNotifications.ts
  • app/src/utils/routeUtils.ts
  • app/src/views/main/MainView.vue
  • app/src/views/main/community/CommunityView.vue
  • app/src/views/main/community/channel/modals/ChangeChannelModal.vue
  • app/src/views/main/community/channel/modals/ManageChannelPluginsModal.vue
  • app/src/views/main/community/sidebar/SidebarItem.vue
  • app/src/views/main/community/sidebar/SidebarList.vue
  • app/src/views/main/modals/CreateCommunityModal.vue
  • app/src/views/signup/Orb.vue
  • package.json
  • packages/api/jest.config.cjs
  • packages/api/package.json
  • packages/api/src/channel/ChannelSummary.test.ts
  • packages/api/src/channel/ChannelSummary.ts
  • packages/api/src/channel/channel-query.test.ts
  • packages/api/src/channel/channel.test.ts
  • packages/api/src/channel/index.ts
  • packages/api/src/conversation-subgroup/index.ts
  • packages/api/src/conversation/conversation.test.ts
  • packages/api/src/conversation/index.ts
  • packages/api/src/conversation/util.ts
  • packages/api/src/createCommunity.ts
  • packages/api/src/index.ts
  • packages/api/src/message/MessageSummary.test.ts
  • packages/api/src/message/MessageSummary.ts
  • packages/api/src/post/index.ts
  • packages/api/src/semantic-relationship/index.ts
  • packages/api/src/subscribeToLinks.ts
  • packages/api/src/task/index.ts
  • packages/api/src/topic/index.ts
  • packages/api/vitest.config.ts
  • packages/comment-section/src/Components/CommentSection/CommentSection.tsx
  • packages/flux-container/package.json
  • packages/flux-container/src/flux-container.ts
  • packages/react-web/src/cache.tsx
  • packages/ui/lib/components/input/Input.js
  • packages/ui/lib/components/tab-item/TabItem.ts
  • packages/ui/meta.json
  • packages/vue/src/usePerspectives.ts
  • scripts/build-with-ad4m-link.sh
  • turbo.json
  • views/chat-view/src/components/MessageList/MessageList.tsx
  • views/graph-view/src/components/CommunityGraph.tsx
  • views/kanban-view/src/components/Board/Board.tsx
  • views/kanban-view/src/components/Board/Task.pl
  • views/kanban-view/src/components/Board/TaskModel.ts
  • views/kanban-view/src/components/Entry/Entry.tsx
  • views/kanban-view/src/hooks/useAssociations.tsx
  • views/poll-view/src/components/PollCard/PollCard.tsx
  • views/synergy-demo-view/src/components/TimelineColumn/TimelineColumn.tsx
  • views/table-view/src/components/Entry/Entry.tsx
  • views/table-view/src/components/Header/Header.tsx
  • views/table-view/src/components/History/History.tsx
  • views/table-view/src/components/NewClass/NewClass.tsx
  • views/table-view/src/components/Table/Table.tsx
  • views/table-view/src/components/TableView/TableView.tsx
💤 Files with no reviewable changes (4)
  • packages/api/jest.config.cjs
  • views/kanban-view/src/components/Board/Task.pl
  • packages/react-web/src/cache.tsx
  • packages/api/src/channel/channel-query.test.ts

Comment thread app/package.json
Comment on lines +203 to +219
(async () => {
try {
const sub = await perspective.subscribeQuery(`
SELECT ?id WHERE { <${channelUrl}> <ad4m://has_child> ?id . }
`);
if (isUnmounted) {
sub.dispose();
return;
}
channelItemsSub = sub;
sub.onResult(() => {
scheduleUnprocessedItemsRefresh();
});
} catch (error) {
console.error('Failed to subscribe to channel items:', error);
}
})();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 💤 Low value

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether TimelineColumn.vue is mounted with a :key forcing remount on channelId change
rg -nP --type=vue -C2 '<TimelineColumn\b' || rg -nP -g '*.vue' -C2 '<TimelineColumn\b'

Repository: coasys/flux

Length of output: 454


🏁 Script executed:

cd app && find . -name "TimelineColumn.vue" -type f

Repository: coasys/flux

Length of output: 113


🏁 Script executed:

cat -n app/src/components/conversation/timeline/TimelineColumn.vue

Repository: coasys/flux

Length of output: 20073


🏁 Script executed:

# Check how channelUrl is derived from route params
rg -n "channelUrl|restoreChannelPrefix" app/src/components/conversation/timeline/TimelineColumn.vue

Repository: coasys/flux

Length of output: 734


🏁 Script executed:

# Check for route watchers or computed properties in TimelineColumn.vue
rg -n "watch|computed.*route" app/src/components/conversation/timeline/TimelineColumn.vue

Repository: coasys/flux

Length of output: 242


🏁 Script executed:

# Check the parent Conversation.vue for route context and whether it forces remount
cat -n app/src/containers/Conversation.vue | head -150

Repository: coasys/flux

Length of output: 7345


🏁 Script executed:

# Check if Conversation.vue has route watching or is remounted on route change
rg -n "watch.*route|channelId" app/src/containers/Conversation.vue | head -20

Repository: coasys/flux

Length of output: 639


🏁 Script executed:

# Find restoreChannelPrefix implementation
fd -name "routeUtils*" app/src

Repository: coasys/flux

Length of output: 286


🏁 Script executed:

# Check for Vue Router configuration to understand navigation behavior
fd -name "*.ts" -o -name "*.js" app/src | xargs rg -l "createRouter\|beforeEach" | head -5

Repository: coasys/flux

Length of output: 286


🏁 Script executed:

# Check if there's a route watcher that forces component updates
rg -n "onBeforeRouteUpdate|onBeforeRouteLeave|watch.*route" app/src/containers/Conversation.vue

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Find routeUtils file
fd routeUtils app/src

Repository: coasys/flux

Length of output: 83


🏁 Script executed:

# Find and display restoreChannelPrefix implementation
rg -n "restoreChannelPrefix" app/src/utils -A 10

Repository: coasys/flux

Length of output: 256


🏁 Script executed:

# Check router configuration
rg -n "createRouter\|beforeEach" app/src

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check Conversation.vue for route watching to see if component remounts on channel change
cat -n app/src/containers/Conversation.vue | sed -n '140,250p'

Repository: coasys/flux

Length of output: 5245


🏁 Script executed:

# Check if Conversation.vue uses useRoute with watchers or lifecycle hooks to handle route changes
rg -n "onBeforeRouteUpdate\|watch.*route\|route\." app/src/containers/Conversation.vue | head -20

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check the full Conversation component setup to see if there's navigation handling
cat -n app/src/containers/Conversation.vue | sed -n '156,300p'

Repository: coasys/flux

Length of output: 7016


🏁 Script executed:

# Look at router configuration to understand navigation behavior
find app/src -name "*router*" -o -name "*routes*" | head -10

Repository: coasys/flux

Length of output: 70


🏁 Script executed:

# Check router configuration files
ls -la app/src/router/

Repository: coasys/flux

Length of output: 227


🏁 Script executed:

# Check the main router file to understand navigation behavior
cat -n app/src/router/index.ts | head -100

Repository: coasys/flux

Length of output: 4028


🏁 Script executed:

# Check if Conversation.vue route is defined with any special configuration
rg -n "Conversation\|conversation" app/src/router/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check for vue-router beforeEach guards that might affect component lifecycle
rg -n "beforeEach\|beforeResolve" app/src/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check ChannelView.vue to see if it handles route param changes
cat -n app/src/views/main/community/channel/ChannelView.vue | head -200

Repository: coasys/flux

Length of output: 2804


🏁 Script executed:

# Look for how Conversation container is used
rg -n "Conversation" app/src/views/main/community/channel/ChannelView.vue

Repository: coasys/flux

Length of output: 137


🏁 Script executed:

# Find ViewView.vue to see what it renders
fd ViewView.vue app/src

Repository: coasys/flux

Length of output: 110


🏁 Script executed:

# Check ViewView.vue to see if it renders Conversation or something else
cat -n app/src/views/main/community/channel/view/ViewView.vue | head -150

Repository: coasys/flux

Length of output: 6385


🏁 Script executed:

# Check if there are any route watchers in ViewView or other components in the hierarchy
rg -n "watch.*route\|onBeforeRouteUpdate" app/src/views/main/community/channel/view/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check where route.params.channelId is used and what values it can have
rg -n "route.params.channelId\|channelId" app/src/utils/routeUtils.ts

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Look at the full routeUtils.ts to see all route helpers
cat -n app/src/utils/routeUtils.ts

Repository: coasys/flux

Length of output: 1571


🏁 Script executed:

# Check if there's any validation or sanitization of route params
rg -n "stripChannelPrefix\|sanitize" app/src/utils/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Check the full Conversation.vue file to see if there are any route watchers we missed
wc -l app/src/containers/Conversation.vue

Repository: coasys/flux

Length of output: 95


🏁 Script executed:

# Search for any route watchers in Conversation.vue around line 300+
cat -n app/src/containers/Conversation.vue | tail -100

Repository: coasys/flux

Length of output: 3148


🏁 Script executed:

# Check if channelId is validated anywhere in the router or component setup
rg -n "channelId.*validate\|channelId.*test\|channelId.*regex" app/src/

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Verify: does the KeepAlive in ChannelView affect remounting behavior?
rg -n "KeepAlive" app/src/views/main/community/channel/ChannelView.vue -B2 -A2

Repository: coasys/flux

Length of output: 404


Add a route watcher or component :key to reset subscriptions when navigating between channels.

The IIFE at lines 203–219 runs once during component setup and captures channelUrl. If Vue Router reuses the TimelineColumn component instance across channel navigations (which occurs without a :key on the component in Conversation.vue:131), the SPARQL subscription, the unprocessed items refresh, and the AI/sidebar watchers all continue pointing at the original channel.

To fix this, either:

  1. Add :key="route.params.channelId" to TimelineColumn in Conversation.vue, or
  2. Add a watcher for route.params.channelId in TimelineColumn.vue to reset and re-issue subscriptions.

Additionally, channelUrl is interpolated directly into the SPARQL query at line 206. While restoreChannelPrefix simply prepends literal:string:, channel IRIs derived from route params are typically safe, but an explicit sanitization or assertion would harden the query against malformed URIs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/conversation/timeline/TimelineColumn.vue` around lines 203
- 219, The TimelineColumn component currently sets up a long-lived SPARQL
subscription using the captured channelUrl inside an IIFE (see the
subscribeQuery block and channelItemsSub), which won’t reset when Vue Router
reuses the component for a different channel; fix by either adding a route-based
key on the parent (e.g., set :key="route.params.channelId" where Conversation
renders TimelineColumn) or by adding a watcher inside TimelineColumn for
route.params.channelId that disposes channelItemsSub, clears any related
watchers (AI/sidebar watchers, unprocessed items logic), and re-runs the
subscription setup (the IIFE logic) when the channel changes; also sanitize or
assert channelUrl before interpolating into the SPARQL string (or use
restoreChannelPrefix consistently) to avoid injecting malformed IRIs into
subscribeQuery.

Comment on lines +210 to +211
const key = perspective.sharedUrl || perspective.uuid;
const perspectiveIsCommunity = Object.keys(myCommunities.value).some((id) => key === id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix key format mismatch with myCommunities store.

The key derivation here uses perspective.sharedUrl || perspective.uuid, but appStore.getMyCommunities() (line 105 in appStore.ts) stores keys as perspective.sharedUrl || 'private://${perspective.uuid}'. This mismatch means perspectives without sharedUrl will never match against myCommunities, incorrectly showing them as "not a flux community".

🐛 Proposed fix to align key format
-    const key = perspective.sharedUrl || perspective.uuid;
+    const key = perspective.sharedUrl || `private://${perspective.uuid}`;
     const perspectiveIsCommunity = Object.keys(myCommunities.value).some((id) => key === id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const key = perspective.sharedUrl || perspective.uuid;
const perspectiveIsCommunity = Object.keys(myCommunities.value).some((id) => key === id);
const key = perspective.sharedUrl || `private://${perspective.uuid}`;
const perspectiveIsCommunity = Object.keys(myCommunities.value).some((id) => key === id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/views/main/modals/CreateCommunityModal.vue` around lines 210 - 211,
The key used to check membership is built incorrectly: change the key derivation
used by perspectiveIsCommunity to match the myCommunities store format (which
uses perspective.sharedUrl || `private://${perspective.uuid}` in
appStore.getMyCommunities()). Update the code that defines key (used by
perspectiveIsCommunity) to use perspective.sharedUrl ||
`private://${perspective.uuid}` so perspectives without a sharedUrl will
correctly match entries in myCommunities.value.

Comment on lines 167 to 168
const valuesClause = unprocessedIds.map((id: string) => `<${id}>`).join(' ');
const dataQuery = `
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape/validate IDs interpolated into the VALUES clause.

unprocessedIds is produced from a prior SPARQL result and then concatenated directly into the data query as <${id}>. If any item id ever contains a > (or other IRI-breaking character) this will produce a malformed query and the whole unprocessedItems() call will fall back to [] (the catch block), causing the AI pipeline to silently stop enqueuing. A simple guard (regex-validate or reject ids containing >/whitespace before interpolation) avoids a hard-to-diagnose outage.

🛡️ Suggested defensive validation
-      const valuesClause = unprocessedIds.map((id: string) => `<${id}>`).join(' ');
+      const safeIds = unprocessedIds.filter(
+        (id: string) => typeof id === 'string' && !/[\s<>"{}|^`\\]/.test(id),
+      );
+      if (safeIds.length === 0) return [];
+      const valuesClause = safeIds.map((id: string) => `<${id}>`).join(' ');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const valuesClause = unprocessedIds.map((id: string) => `<${id}>`).join(' ');
const dataQuery = `
const safeIds = unprocessedIds.filter(
(id: string) => typeof id === 'string' && !/[\s<>"{}|^`\\]/.test(id),
);
if (safeIds.length === 0) return [];
const valuesClause = safeIds.map((id: string) => `<${id}>`).join(' ');
const dataQuery = `
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/api/src/channel/index.ts` around lines 167 - 168, The interpolation
of unprocessedIds into valuesClause (`unprocessedIds.map(id => `<${id}>`)`) is
unsafe; validate or escape each id before building the SPARQL VALUES clause
(e.g., reject or log and skip ids containing '>', whitespace, or other
IRI-breaking characters) to avoid constructing a malformed query that causes
unprocessedItems() to fail silently; update the code that produces valuesClause
to validate each id against a safe pattern (or explicitly escape/URN-encode it),
and ensure failures are handled deterministically (throw or return a clear
error/log) rather than falling back to an empty array.

Comment on lines +261 to +308
const sparql = `
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
SELECT ?channelId (SAMPLE(?cId) AS ?conversationId) (MAX(?ts) AS ?lastActivity) WHERE {
?channelId <${ENTRY_TYPE}> <${EntryType.Channel}> .
?channelId <${CHANNEL_IS_CONVERSATION}> ?_isConv .
FILTER(STR(<ad4m://fn/parse_literal>(?_isConv)) = "true")
OPTIONAL {
?channelId <ad4m://has_child> ?cId .
?cId <flux://entry_type> <flux://conversation> .
}
OPTIONAL {
?channelId <ad4m://has_child> ?item .
?_itemReifier rdf:reifies <<( ?channelId <ad4m://has_child> ?item )>> .
?_itemReifier <ad4m://ontology/timestamp> ?itemTs .
?item <${ENTRY_TYPE}> ?itemType .
FILTER(?itemType IN (<${EntryType.Message}>, <${EntryType.Post}>))
}
OPTIONAL {
?_chanReifier rdf:reifies <<( ?_parent <flux://has_channel> ?channelId )>> .
?_chanReifier <ad4m://ontology/timestamp> ?chanCreatedTs .
}
BIND(COALESCE(?itemTs, ?chanCreatedTs, "1970-01-01T00:00:00Z"^^<http://www.w3.org/2001/XMLSchema#dateTime>) AS ?ts)
}
GROUP BY ?channelId
ORDER BY DESC(?lastActivity)
LIMIT ${limit}
`;

try {
const results = await perspective.querySparql(sparql);
// Safety-net dedup by channelId — the SPARQL GROUP BY should already
// return one row per channel, but guard against engine quirks.
const seen = new Map<string, { channelId: string; conversationId?: string; lastActivity?: string }>();
for (const r of results || []) {
const cid = r.channelId;
if (!cid || seen.has(cid)) continue;
seen.set(cid, {
channelId: cid,
conversationId: r.conversationId || undefined,
lastActivity: r.lastActivity || undefined,
});
}
return Array.from(seen.values());
} catch (error) {
console.error('Error in Channel.recentConversations():', error);
return [];
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does @coasys/ad4m0.13.x Oxigraph expose a custom SPARQL functionad4m://fn/parse_literal for use in FILTER expressions?

💡 Result:

No, @coasys/ad4m 0.13.x with Oxigraph does not expose a custom SPARQL function ad4m://fn/parse_literal for use in FILTER expressions. Oxigraph implements standard SPARQL 1.1 with some extensions, but no custom AD4M functions like ad4m://fn/parse_literal are documented in its SPARQL support [1][2]. AD4M primarily uses SurrealDB for querying, where a similar fn::parse_literal() function exists for parsing AD4M literal URIs (e.g., literal://string:Hello → "Hello") [3]. This SurrealQL function is used in AD4mModel queries and direct perspective.querySurrealDB() calls, not SPARQL [4][5][6]. No evidence in AD4M GitHub repo [7][8][9], changelog [9], releases up to v0.13.0-test-1 [8], or docs mentions Oxigraph integration or custom SPARQL functions [6]. AD4M's query engine defaults to SurrealDB (10-100x faster than Prolog), with Prolog as fallback [3][4]. If Oxigraph is used, it would support standard SPARQL functions only [1].

Citations:


Remove or replace the unsupported ad4m://fn/parse_literal SPARQL function.

The STR(<ad4m://fn/parse_literal>(?_isConv)) filter will fail at runtime because ad4m://fn/parse_literal is not a supported SPARQL function in Oxigraph. AD4M uses SurrealDB by default and only supports standard SPARQL 1.1 functions when Oxigraph is used. The function exists as fn::parse_literal() in SurrealQL, not SPARQL. This will cause both recentConversations and pinnedConversations to throw an exception, caught silently and returning [], hiding all pinned/recent conversations from the UI.

Additionally, the OPTIONAL block (lines 271–277) expands the join to every channel item before GROUP BY collapses it; correctness is preserved by MAX(?ts)/SAMPLE(?cId), but result-set size grows linearly with item count per channel.

Comment on lines +31 to +34
const { data: answers } = useLiveQuery(Answer, perspective, {
parent: { model: Poll, id: poll.id },
query: { include: { votes: true } },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify useLiveQuery API and include syntax in ad4m-react-hooks

# Check the actual version being used
cat package.json | jq '.dependencies["@coasys/ad4m-react-hooks"], .devDependencies["@coasys/ad4m-react-hooks"]'

# Search for other usages of include in queries to see if pattern is used elsewhere
rg -n "useLiveQuery.*include" --type=ts --type=tsx -C2

Repository: coasys/flux

Length of output: 111


🌐 Web query:

@coasys/ad4m-react-hooks useLiveQuery include option documentation version 0.13.0-test-4

💡 Result:

There isn’t any documentation I can find (from the official AD4M docs/pages that describe useLiveQuery) that mentions an include option for @coasys/ad4m-react-hooks useLiveQuery—so I can’t provide “include option documentation” for version 0.13.0-test-4 specifically. What the official docs do say about useLiveQuery: - useLiveQuery is documented in AD4M’s “AD4M React Hooks” page, describing collection mode (default), single-instance mode (pass an id in options), and other features like parent-scoped queries, pagination, and return types. [1] - The docs page shows useLiveQuery(Todo, perspective, { query: { where: ... }, pageSize: ... }), but the visible options shown there do not include an include field. [1] Why this matters / where “include” likely fits instead: - AD4M’s query system supports eager-loading via an include field in the underlying model/query type (include?: IncludeMap), and IncludeMap is documented as controlling relation hydration (e.g., { comments: true }, or scoped sub-queries). [2] - So, if you want “include” behavior with useLiveQuery, the most likely path is that useLiveQuery forwards a query object down to the model query builder, and therefore query.include (not a top-level useLiveQuery option named include) is the correct place. However, I did not find a source that explicitly states this mapping for 0.13.0-test-4. If you want, paste the exact useLiveQuery call you’re using and I’ll map which option shape should contain include based on AD4M’s Query/IncludeMap types. [2]

Citations:


🏁 Script executed:

#!/bin/bash
# Find package.json files and check ad4m-react-hooks version
fd -n "package.json" -x cat {} \; | grep -A2 -B2 "ad4m-react-hooks"

Repository: coasys/flux

Length of output: 286


🏁 Script executed:

#!/bin/bash
# Search for useLiveQuery usage patterns (fix file type issue)
rg -n "useLiveQuery.*include" -A2 -B2

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Check for type definitions or docs in the repo about useLiveQuery
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.d.ts" \) | xargs grep -l "useLiveQuery" | head -10

Repository: coasys/flux

Length of output: 635


🏁 Script executed:

#!/bin/bash
# Find and display package.json with ad4m-react-hooks dependency
find . -name "package.json" -type f -exec grep -l "ad4m-react-hooks" {} \;

Repository: coasys/flux

Length of output: 462


🏁 Script executed:

#!/bin/bash
# Look at actual useLiveQuery calls in PollCard and other files
rg "useLiveQuery" -A4 -B1 views/poll-view/src/components/PollCard/PollCard.tsx views/chat-view/src/components/MessageList/MessageList.tsx views/kanban-view/src/components/Card/Card.tsx

Repository: coasys/flux

Length of output: 3336


🏁 Script executed:

#!/bin/bash
# Check package.json in the root and views directories
cat package.json 2>/dev/null | grep -A1 -B1 "ad4m-react-hooks" || echo "Not found in root"
for dir in views/*/; do
  [ -f "$dir/package.json" ] && echo "=== $dir ===" && grep -A1 -B1 "ad4m-react-hooks" "$dir/package.json"
done

Repository: coasys/flux

Length of output: 1602


🏁 Script executed:

#!/bin/bash
# Check if poll-view package.json should be updated
# Look at root package.json and see if this might be part of the PR
cat package.json | grep -A5 -B5 "ad4m-react-hooks"

Repository: coasys/flux

Length of output: 337


🏁 Script executed:

#!/bin/bash
# Get full PollCard.tsx to understand the complete implementation
wc -l views/poll-view/src/components/PollCard/PollCard.tsx

Repository: coasys/flux

Length of output: 112


🏁 Script executed:

#!/bin/bash
# Check if poll-view has a separate yarn lock or if it uses root
cat views/poll-view/package.json | head -20

Repository: coasys/flux

Length of output: 506


🏁 Script executed:

#!/bin/bash
# Read full PollCard.tsx to see the implementation
cat -n views/poll-view/src/components/PollCard/PollCard.tsx | head -50

Repository: coasys/flux

Length of output: 2156


🏁 Script executed:

#!/bin/bash
# Check if there's a pnpm-workspace or monorepo config
ls -la | grep -E "pnpm-workspace|package-lock|yarn.lock"
cat pnpm-workspace.yaml 2>/dev/null || echo "No pnpm-workspace.yaml found"

Repository: coasys/flux

Length of output: 206


🏁 Script executed:

#!/bin/bash
# Check git status to see if package.json updates are part of this change
git diff HEAD -- views/poll-view/package.json 2>/dev/null || echo "Not a git repo or no diff"
git log --oneline -5 2>/dev/null || echo "Cannot access git log"

Repository: coasys/flux

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Read more of PollCard.tsx to see how include is used
cat -n views/poll-view/src/components/PollCard/PollCard.tsx | sed -n '47,120p'

Repository: coasys/flux

Length of output: 3401


🏁 Script executed:

#!/bin/bash
# Check views/poll-view/package.json dependencies more thoroughly
cat views/poll-view/package.json

Repository: coasys/flux

Length of output: 1239


🏁 Script executed:

#!/bin/bash
# Check if there are any type errors or TypeScript config issues
find views/poll-view -name "tsconfig.json" -o -name "tsconfig.app.json" | xargs cat 2>/dev/null

Repository: coasys/flux

Length of output: 466


Update views/poll-view/package.json to use @coasys/ad4m-react-hooks version 0.13.0-test-4 to support the include query syntax.

The code at lines 31-34 uses query: { include: { votes: true } } to eager-load votes with the subscription, but views/poll-view/package.json is locked to version 0.11.1 while the root package.json has been bumped to 0.13.0-test-4. In a pnpm monorepo, this version mismatch will cause the include syntax to fail at runtime.

Additionally, line 98 should use the already-loaded votes from the subscription instead of calling Vote.findAll():

Suggested fix for line 98
  async function vote(answerId: string, value?: number) {
-   const votes = await Vote.findAll(perspective, { parent: { model: Answer, id: answerId } });
+   const answer = answers.find(a => a.id === answerId);
+   const votes = answer?.votes || [];
    const previousVote = votes.find((vote: any) => vote.author === myDid) as any;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/poll-view/src/components/PollCard/PollCard.tsx` around lines 31 - 34,
The project is using the new include query syntax with useLiveQuery(Answer,
perspective, { parent: { model: Poll, id: poll.id }, query: { include: { votes:
true } } }) but views/poll-view/package.json is pinned to
`@coasys/ad4m-react-hooks` v0.11.1 which lacks that syntax; update
views/poll-view/package.json to use `@coasys/ad4m-react-hooks` version
0.13.0-test-4 so the include option works, and modify the render code that
currently calls Vote.findAll() (referenced as Vote.findAll) to instead use the
eager-loaded votes from the subscription (access votes on the Answer objects
returned by useLiveQuery, e.g., answers -> answer.votes) so you reuse the loaded
data rather than refetching.

Comment on lines 40 to 44
async function addClass() {
const sdna = await generateSDNA(name, columns, perspective);
await perspective.addSdna(sdna);
const shape = await buildSHACLShape(name, columns, perspective);
await perspective.addShacl(name, shape);
onSaved();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate and sanitize before persisting the SHACL shape.

addClass() currently saves whatever is in state. There are several reachable bad states:

  • name is not validated; it can be empty, or contain spaces / # / /, which produces a malformed IRI in recipe://${name} and a broken class id ${ns}#${name} further down.
  • New columns are added with empty name and empty predicate (see addNewColumn). Persisting a property shape whose path is '' is almost certainly invalid SHACL.
  • Two columns with the same predicate would overwrite each other semantically.

Recommend at minimum validating that name is non-empty and IRI-safe, every column has a non-empty name and predicate, and predicates are unique — and surface the error before calling perspective.addShacl.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/NewClass/NewClass.tsx` around lines 40 - 44,
In addClass(), validate and sanitize inputs before calling
buildSHACLShape/perspective.addShacl: ensure the class name (variable name) is
non-empty and URL/IRI-safe (no spaces, '#' or '/'; escape or reject invalid
chars), ensure every column in columns (added via addNewColumn) has non-empty
name and predicate, and enforce uniqueness of column predicates (reject or mark
duplicates), then surface a clear validation error to the user (do not call
perspective.addShacl or onSaved until validation passes); run these checks
immediately in addClass and only call buildSHACLShape(passedName, passedColumns,
perspective) and perspective.addShacl when all validations succeed.

Comment on lines +227 to 276
async function buildSHACLShape(
name: string,
columns: Record<string, Column>,
perspective: PerspectiveProxy,
): Promise<string> {
const atom = makeRandomPrologAtom(6);
let constructorActions = '';
let instancePredicates = '';
let propertiesString = '';

for await (const column of Object.values(columns)) {
const { name: columnName, required, predicate, defaultValue, language } = column;

const expression = await perspective.createExpression(defaultValue, language);

if (defaultValue) {
constructorActions += `{action: "addLink", source: "this", predicate: "${predicate}", target: "${expression}"},`;
}

if (required) {
instancePredicates += `triple(Base, "${predicate}", _),`;
): Promise<SHACLShape> {
const ns = `recipe://${name}`;
const shape = new SHACLShape(`${ns}Shape`, `${ns}#${name}`);
const constructorActions: Array<{ action: string; source: string; predicate: string; target: string }> = [];

for (const column of Object.values(columns)) {
const { name: columnName, required, predicate, defaultValue, language, options } = column;

const expression = defaultValue ? await perspective.createExpression(defaultValue, language) : undefined;

const propShape: any = {
name: columnName,
path: predicate,
datatype: 'xsd://string',
minCount: required ? 1 : 0,
maxCount: 1,
writable: true,
};

if (language) {
propShape.resolveLanguage = language;
}

/*
if (required && column.options.length > 0) {
for (const option of column.options) {
instancePredicates += `triple(Base, "${predicate}", "${option.value}");`;
}
if (options.length > 0) {
propShape.in = options.map((opt) => ({ value: opt.value, label: opt.name }));
}
*/

let namedOptionsString = '';
shape.addProperty(propShape);

for (const option of column.options) {
namedOptionsString += `property_named_option(${atom}, "${columnName}", "${option.value}", "${option.name}").\n`;
// Build constructor action for default value
if (expression) {
constructorActions.push({
action: 'addLink',
source: 'this',
predicate,
target: expression,
});
}

propertiesString += `
property(${atom}, "${columnName}").
property_resolve(${atom}, "${columnName}").
property_resolve_language(${atom}, "${columnName}", "${language}").
property_getter(${atom}, Base, "${columnName}", Value) :- triple(Base, "${predicate}", Value).
property_setter(${atom}, "${columnName}", '[{action: "setSingleTarget", source: "this", predicate: "${predicate}", target: "value"}]').
${namedOptionsString}
`;
}

// Replace the last comma with a space to format the Prolog string properly
constructorActions = constructorActions.replace(/,*$/, '');
instancePredicates = instancePredicates.replace(/,*$/, '');
// Replace the or operator (;)
instancePredicates = instancePredicates.replace(/;*$/, '');

const prologString = `
subject_class("${name}", ${atom}).
constructor(${atom}, '[${constructorActions}]').
instance(${atom}, Base) :- ${instancePredicates}.
${propertiesString}
`;
if (constructorActions.length > 0) {
shape.setConstructorActions(constructorActions);
}

return prologString;
return shape;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In @coasys/ad4m 0.13.0, what is the SHACLShape API (constructor args, addProperty schema, setConstructorActions placeholders), and what datatype URI is expected for strings?

💡 Result:

In @coasys/ad4m 0.13.0, the SHACLShape API is: 1) Constructor args - new SHACLShape(targetClassOrShapeUri, targetClass?) [1] - If you pass one argument, it is the target class; the shape URI is auto-derived as {class}Shape [1] - If you pass two arguments: first is the shape URI and second is the target class [1] 2) addProperty schema - Method signature: addProperty(prop: SHACLPropertyShape): void [1] - You add a property constraint by pushing the SHACLPropertyShape into the shape’s properties array [2]. 3) setConstructorActions placeholders - Method signature: setConstructorActions(actions: AD4MAction[]): void [1] - It sets the AD4M-specific constructor_actions field on the shape object [2]. 4) Datatype URI expected for strings - For string values, the generated SHACL uses sh:datatype xsd:string, and AD4M stores those as literal://string:... URIs [3]. So, for “strings”, use xsd:string (SHACL datatype) / the corresponding AD4M literal string encoding literal://string:... [3].

Citations:


🏁 Script executed:

# Search for AD4M SHACLShape and related SHACL implementations
curl -s "https://raw.githubusercontent.com/coasys/ad4m/main/core/src/shacl/SHACLShape.ts" | head -200

Repository: coasys/flux

Length of output: 6102


🏁 Script executed:

# Search for AD4M constructor actions documentation
curl -s "https://raw.githubusercontent.com/coasys/ad4m/main/core/src/types.ts" | grep -A 20 "constructor_actions\|AD4MAction" | head -50

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Search for datatype handling in AD4M SHACL
curl -s "https://raw.githubusercontent.com/coasys/ad4m/main/core/src/shacl/SHACLPropertyShape.ts" | head -150

Repository: coasys/flux

Length of output: 69


🏁 Script executed:

# Search for 'in' field in SHACL property shape definitions
rg "in\?" views/table-view/src/components --type ts -A 2 -B 2 | head -100

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Search for propShape definitions in the file to understand the structure
rg "propShape\." views/table-view/src/components/NewClass/NewClass.tsx -A 1 -B 1

Repository: coasys/flux

Length of output: 248


🏁 Script executed:

# Search for perspective.addShacl calls to understand the method signature
rg "addShacl|setConstructorActions" views/table-view/src --type ts -B 2 -A 2

Repository: coasys/flux

Length of output: 859


🏁 Script executed:

# Search for how AD4M handles datatype URIs in examples or documentation
rg "datatype.*string|xsd:" views/table-view/src --type ts | head -20

Repository: coasys/flux

Length of output: 141


🏁 Script executed:

# Search for datatype examples in the codebase
rg "datatype.*:" views/table-view/src --type ts -B 1 -A 1

Repository: coasys/flux

Length of output: 306


🏁 Script executed:

# Search for constructor actions in the codebase to understand 'this' usage
rg "action.*addLink|source.*this" views/table-view/src --type ts -B 2 -A 2

Repository: coasys/flux

Length of output: 531


🏁 Script executed:

# Look for any documentation or type definitions of perspective methods
fd -e ts -e tsx views/table-view/src | xargs grep -l "perspective\." | head -3

Repository: coasys/flux

Length of output: 441


🏁 Script executed:

# Search for PerspectiveProxy type definition
fd . views/table-view/src --full-path -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "PerspectiveProxy\|perspective\.addShacl" | head -5

Repository: coasys/flux

Length of output: 226


🏁 Script executed:

# Search for any imports or type definitions related to perspective
rg "import.*Perspective|type.*Perspective" views/table-view/src --type ts -A 2 | head -50

Repository: coasys/flux

Length of output: 1976


🏁 Script executed:

# Look for example SHACL shapes in the codebase
rg "SHACLShape\|addProperty" views/table-view/src --type ts -B 1 -A 3 | head -80

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Search for any existing SHACL shape examples in the codebase
rg "new SHACLShape|addProperty|setConstructorActions" . --type ts --type tsx -B 2 -A 5 | head -100

Repository: coasys/flux

Length of output: 83


🏁 Script executed:

# Look for how datatype is handled elsewhere in the codebase
rg "datatype" . --type ts --type tsx -B 1 -A 1 | head -50

Repository: coasys/flux

Length of output: 83


🏁 Script executed:

# Search for any test files that might show expected formats
fd -type f -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "SHACL\|datatype" 2>/dev/null | head -5

Repository: coasys/flux

Length of output: 226


🏁 Script executed:

# Search for SHACL shape examples in the entire repo
rg "new SHACLShape" -A 5 -B 1 | head -80

Repository: coasys/flux

Length of output: 786


🏁 Script executed:

# Search for datatype usage patterns
rg "datatype:" -A 1 -B 1 | head -60

Repository: coasys/flux

Length of output: 306


🏁 Script executed:

# Search for any test or example files with SHACL
rg "SHACLShape\|addProperty" -A 3 -B 1 | head -100

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Look for any documentation or comments about the expected format
rg "xsd:|datatype|datatype.*string" -B 2 -A 1 | head -80

Repository: coasys/flux

Length of output: 384


🏁 Script executed:

# Search for perspective method definitions or type definitions
rg "addShacl.*function|addShacl.*method" . -B 2 -A 5

Repository: coasys/flux

Length of output: 37


🏁 Script executed:

# Look for any imports from ad4m to understand the API surface
rg "from.*@coasys/ad4m" . -A 3 | head -100

Repository: coasys/flux

Length of output: 8070


🏁 Script executed:

# Search for any comments or documentation about SHACL in the codebase
rg "SHACL|datatype|constructor" . -B 1 -A 1 | grep -i "comment\|doc\|//" | head -50

Repository: coasys/flux

Length of output: 31631


Fix datatype URI format from xsd://string to xsd:string.

Line 244 uses datatype: 'xsd://string', but AD4M 0.13.0's SHACL parser expects standard CURIE notation: datatype: 'xsd:string'. The xsd:// scheme is non-standard; the correct format for XSD datatypes in SHACL is the CURIE prefix xsd: (or the full URI http://www.w3.org/2001/XMLSchema#string).

The other concerns remain valid and worth confirming:

  • Constructor action source: 'this' (Line 264) — verify AD4M's constructor-action engine correctly substitutes 'this' with the new subject.
  • propShape.in format (Line 255) using { value, label } objects — confirm this AD4M extension is supported.
  • perspective.addShacl(name, shape) signature — verify the arity and parameter order match AD4M's API.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/NewClass/NewClass.tsx` around lines 227 -
276, The SHACL property datatype is using a non-standard URI 'xsd://string'
inside buildSHACLShape; change the propShape.datatype assignment to use the
CURIE 'xsd:string' (or full XSD URI) so the SHACL parser accepts it; update the
line that sets datatype in the propShape object (where propShape is constructed
in buildSHACLShape) to 'xsd:string', keep the rest of the propShape
construction, addProperty calls, and constructorActions logic unchanged, and
then re-run tests to confirm parser accepts the shape.

Comment on lines +232 to +233
const ns = `recipe://${name}`;
const shape = new SHACLShape(`${ns}Shape`, `${ns}#${name}`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape name before embedding it in the recipe IRI.

recipe://${name} is built by raw string concatenation. Any whitespace, #, /, or other reserved IRI character that a user types will produce an invalid IRI and a malformed targetClass (${ns}#${name}). Sanitize/percent-encode the name (or restrict the input to a safe character class) before constructing the namespace.

-  const ns = `recipe://${name}`;
-  const shape = new SHACLShape(`${ns}Shape`, `${ns}#${name}`);
+  const safeName = encodeURIComponent(name.trim());
+  if (!safeName) throw new Error('Class name is required');
+  const ns = `recipe://${safeName}`;
+  const shape = new SHACLShape(`${ns}Shape`, `${ns}#${safeName}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ns = `recipe://${name}`;
const shape = new SHACLShape(`${ns}Shape`, `${ns}#${name}`);
const safeName = encodeURIComponent(name.trim());
if (!safeName) throw new Error('Class name is required');
const ns = `recipe://${safeName}`;
const shape = new SHACLShape(`${ns}Shape`, `${ns}#${safeName}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/NewClass/NewClass.tsx` around lines 232 -
233, The code builds an IRI using the raw user-provided name (const ns =
`recipe://${name}`; new SHACLShape(`${ns}Shape`, `${ns}#${name}`)) which can
produce invalid IRIs; sanitize/percent-encode the name before embedding it (or
validate/restrict to a safe char class) and use the encoded value when
constructing ns and the shape IRI (i.e., apply the same encoding helper to name
for both the namespace and the `${ns}#${name}` target so both IRIs are valid).

Comment on lines 61 to 66
useEffect(() => {
perspective.infer(`subject_class(ClassName, C)`).then((result) => {
if (Array.isArray(result)) {
const uniqueClasses = [...new Set(result.map((c) => c.ClassName))];
setClasses(uniqueClasses);
setSelected(uniqueClasses[0] || '');
} else {
setClasses([]);
setSelected('');
}
perspective.listRegisteredClasses().then((result) => {
setClasses(result);
setSelected(result[0] || '');
});
}, [perspective.uuid]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard the "+" create button when no class is selected.

If listRegisteredClasses() returns an empty array, selected is set to ''. The header's + {selected} button still renders and clicking it calls createEntry({ subjectClass: '' }), which will either fail in createSubject or create a subject without a class. Either disable the button when !selected or hide it when classes.length === 0.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@views/table-view/src/components/TableView/TableView.tsx` around lines 61 -
66, When perspective.listRegisteredClasses() yields an empty array the component
sets selected to '' but the header "+" button still renders and can call
createEntry({ subjectClass: '' }); update the header button rendering logic in
the TableView component to guard against no selection: check classes.length and
selected and either disable the "+" button when !selected or hide the button
entirely when classes.length === 0 so createEntry is never invoked with an empty
subjectClass; locate the UI that renders the "+" button (references: selected,
classes, createEntry, createSubject) and add the conditional
rendering/disabled-state there.

jhweir added 2 commits May 12, 2026 14:19
Picks up the embedded mode URL fix — when WE is connected to a remote
node the correct host URL is now forwarded in AD4M_CONFIG so Flux's
WebSocket connects to the right endpoint instead of localhost.
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