diff --git a/ROADMAP.md b/ROADMAP.md
index 3a1a27e..53e00d1 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -110,6 +110,15 @@ defines the Opportunity experience, introduces the ACE-owned design system, and
visual What / Why / How / When grammar across Briefs and decision-facing intelligence. It does not change the 0.9
Collaborative Intelligence promise.
+Current post-0.8.1 candidate work adds the Atrium Intelligence Catalog as an experience entry
+slice: the generic resource consumer enumerates all admitted onboarding profiles in product scope,
+deduplicates them by profile identity, and always offers the Core-owned Custom Intelligence path.
+World and Marketing contribute their own declarative profiles from their independent repositories;
+Core names neither domain and grants no installation, connection, monitor, or activation authority.
+This makes first use outcome-led across domains while leaving live selection/session binding and
+activation on the existing governed Builder boundaries. It is preparatory product work, not a
+claim that the 0.9 collaboration release gate has passed.
+
The prior [0.7.0 GitHub Release](https://github.com/augmented-cognition-engine/core/releases/tag/v0.7.0)
and [`ace-core==0.7.0`](https://pypi.org/project/ace-core/0.7.0/) package remain the passed
Intelligence Builder Foundation boundary, recorded by the
diff --git a/ace/intelligence/contracts/__init__.py b/ace/intelligence/contracts/__init__.py
index b02e15e..ca5781c 100644
--- a/ace/intelligence/contracts/__init__.py
+++ b/ace/intelligence/contracts/__init__.py
@@ -265,6 +265,7 @@
IntelligenceOnboardingGuardrailsV1Alpha1,
IntelligenceOnboardingOutcomeV1Alpha1,
IntelligenceOnboardingProfileV1Alpha1,
+ IntelligenceOnboardingSourceGroupV1Alpha1,
)
from ace.intelligence.contracts.ledger import (
AttentionDisposition,
@@ -481,6 +482,7 @@
"IntelligenceOnboardingGuardrailsV1Alpha1",
"IntelligenceOnboardingOutcomeV1Alpha1",
"IntelligenceOnboardingProfileV1Alpha1",
+ "IntelligenceOnboardingSourceGroupV1Alpha1",
"MAX_RESOURCE_PLANE_PAGE_SIZE",
"RESOURCE_PLANE_CURSOR_VERSION",
"RESOURCE_PLANE_PAGE_VERSION",
diff --git a/ace/intelligence/contracts/intelligence_builder_presentation.py b/ace/intelligence/contracts/intelligence_builder_presentation.py
index 7ff322b..de0ae59 100644
--- a/ace/intelligence/contracts/intelligence_builder_presentation.py
+++ b/ace/intelligence/contracts/intelligence_builder_presentation.py
@@ -78,6 +78,39 @@ def cadence_slug(cls, value: str) -> str:
return validate_slug(value, name="cadence_id")
+class IntelligenceOnboardingSourceGroupV1Alpha1(_BuilderPresentationContract):
+ """One inert, reviewable group of evidence sources proposed during onboarding."""
+
+ source_group_id: str
+ label: str = Field(min_length=1, max_length=160)
+ description: str = Field(min_length=1, max_length=1_000)
+ evidence_role: str
+ source_ids: tuple[str, ...] = Field(min_length=1, max_length=32)
+ source_labels: tuple[str, ...] = Field(min_length=1, max_length=8)
+ access_label: str = Field(min_length=1, max_length=160)
+ default_selected: StrictBool = True
+
+ @field_validator("source_group_id", "evidence_role")
+ @classmethod
+ def slugs(cls, value: str, info) -> str:
+ return validate_slug(value, name=info.field_name)
+
+ @field_validator("source_ids", mode="before")
+ @classmethod
+ def ids(cls, value: Any) -> tuple[str, ...]:
+ return tuple(
+ validate_slug(item, name="source_ids") for item in normalized_strings(value, label="source_ids", maximum=32)
+ )
+
+ @field_validator("source_labels", mode="before")
+ @classmethod
+ def labels(cls, value: Any) -> tuple[str, ...]:
+ return tuple(
+ _text(item, name="source_labels", maximum=160)
+ for item in normalized_strings(value, label="source_labels", maximum=8)
+ )
+
+
class IntelligenceOnboardingFirstValueV1Alpha1(_BuilderPresentationContract):
public_sources_first: StrictBool = True
private_sources_optional: StrictBool = True
@@ -99,10 +132,14 @@ class IntelligenceOnboardingProfileV1Alpha1(_BuilderPresentationContract):
profile_id: str
profile_digest: str | None = None
topic_id: str
+ domain_label: str | None = Field(default=None, min_length=1, max_length=160)
+ topic_label: str | None = Field(default=None, min_length=1, max_length=160)
display_name: str = Field(min_length=1, max_length=160)
prompt: str = Field(min_length=1, max_length=300)
description: str = Field(min_length=1, max_length=2_000)
+ starter_prompts: tuple[str, ...] = Field(default_factory=tuple, max_length=8)
outcomes: tuple[IntelligenceOnboardingOutcomeV1Alpha1, ...] = Field(min_length=1, max_length=16)
+ source_groups: tuple[IntelligenceOnboardingSourceGroupV1Alpha1, ...] = Field(default_factory=tuple, max_length=16)
cadences: tuple[IntelligenceOnboardingCadenceV1Alpha1, ...] = Field(min_length=1, max_length=16)
default_cadence_id: str
first_value: IntelligenceOnboardingFirstValueV1Alpha1
@@ -123,6 +160,14 @@ def digest(cls, value: str | None) -> str | None:
def profile_slugs(cls, value: str, info) -> str:
return validate_slug(value, name=info.field_name)
+ @field_validator("starter_prompts", mode="before")
+ @classmethod
+ def prompts(cls, value: Any) -> tuple[str, ...]:
+ return tuple(
+ _text(item, name="starter_prompts", maximum=300)
+ for item in normalized_strings(value, label="starter_prompts", maximum=8)
+ )
+
@field_validator("outcomes")
@classmethod
def unique_outcomes(
@@ -137,6 +182,18 @@ def unique_cadences(
) -> tuple[IntelligenceOnboardingCadenceV1Alpha1, ...]:
return sorted_unique(value, key=lambda item: item.cadence_id, label="onboarding cadences", maximum=16)
+ @field_validator("source_groups")
+ @classmethod
+ def unique_source_groups(
+ cls, value: tuple[IntelligenceOnboardingSourceGroupV1Alpha1, ...]
+ ) -> tuple[IntelligenceOnboardingSourceGroupV1Alpha1, ...]:
+ return sorted_unique(
+ value,
+ key=lambda item: item.source_group_id,
+ label="onboarding source groups",
+ maximum=16,
+ )
+
@model_validator(mode="after")
def bind_profile(self) -> Self:
if self.default_cadence_id not in {item.cadence_id for item in self.cadences}:
@@ -156,4 +213,5 @@ def bind_profile(self) -> Self:
"IntelligenceOnboardingGuardrailsV1Alpha1",
"IntelligenceOnboardingOutcomeV1Alpha1",
"IntelligenceOnboardingProfileV1Alpha1",
+ "IntelligenceOnboardingSourceGroupV1Alpha1",
]
diff --git a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx
index ad64acf..91874e2 100644
--- a/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx
+++ b/core/ui/canvas/src/app/atrium/IntelligenceOS.tsx
@@ -31,8 +31,7 @@ import { KernelNav } from '../ext/defaults/KernelNav'
import { AskAce } from './AskAce'
import { OnboardingPreview } from './OnboardingPreview'
import {
- hasOnboardingProfileResource,
- onboardingProfileFromResources,
+ onboardingProfilesFromResources,
onboardingSessionFromResources,
} from './onboardingModel'
import { pageFreshness, productDisplayName } from './experienceModel'
@@ -426,9 +425,8 @@ export function IntelligenceOS() {
const productName = productDisplayName(page?.product_id)
const freshness = pageFreshness(page)
const [onboardingOpen, setOnboardingOpen] = useState(false)
- const onboardingProfile = useMemo(() => onboardingProfileFromResources(page?.items ?? []), [page?.items])
+ const onboardingProfiles = useMemo(() => onboardingProfilesFromResources(page?.items ?? []), [page?.items])
const onboardingSession = useMemo(() => onboardingSessionFromResources(page?.items ?? []), [page?.items])
- const hasOnboarding = useMemo(() => hasOnboardingProfileResource(page?.items ?? []), [page?.items])
function openFirstBrief() {
requestAnimationFrame(() => document.getElementById('latest-brief')?.scrollIntoView({ behavior: 'smooth' }))
@@ -451,12 +449,10 @@ export function IntelligenceOS() {
- {hasOnboarding && (
- setOnboardingOpen(true)}>
-
- {onboardingSession === null ? 'Build intelligence' : 'View build'}
-
- )}
+ setOnboardingOpen(true)}>
+
+ {onboardingSession === null ? 'Build intelligence' : 'View build'}
+
{page !== null && (
{page.state === 'degraded' ? : }
@@ -512,7 +508,7 @@ export function IntelligenceOS() {
diff --git a/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx
index 8114c9c..1faa797 100644
--- a/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx
+++ b/core/ui/canvas/src/app/atrium/OnboardingPreview.tsx
@@ -1,14 +1,23 @@
-import { useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import {
ArrowLeft,
ArrowRight,
BarChart3,
+ BookOpenCheck,
Check,
CircleDot,
Compass,
+ Database,
+ FileCheck2,
FlaskConical,
Gauge,
+ GitFork,
+ Landmark,
+ LineChart,
LoaderCircle,
+ LockKeyhole,
+ Megaphone,
+ PlugZap,
Radar,
Scale,
ShieldAlert,
@@ -20,11 +29,13 @@ import { Badge } from '@/design/shadcn/ui/badge'
import { Button } from '@/design/shadcn/ui/button'
import { Card, CardContent } from '@/design/shadcn/ui/card'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/design/shadcn/ui/dialog'
+import { Textarea } from '@/design/shadcn/ui/textarea'
import type {
IntelligenceBuilderSession,
IntelligenceBuilderStage,
IntelligenceOnboardingOutcome,
IntelligenceOnboardingProfile,
+ IntelligenceOnboardingSourceGroup,
} from './onboardingModel'
const ICONS: Record = {
@@ -36,6 +47,17 @@ const ICONS: Record = {
custom: Compass,
}
+const SOURCE_ICONS: Record = {
+ authoritative_record: Landmark,
+ first_party_claim: Megaphone,
+ independent_measurement: LineChart,
+ operational_telemetry: Database,
+ leading_indicator: GitFork,
+ private_organizational: LockKeyhole,
+}
+
+const STEP_LABELS = ['Intent', 'Evidence', 'Focus', 'Review', 'Build'] as const
+
type BuildState = 'complete' | 'active' | 'blocked' | 'waiting' | 'proposed'
interface BuildLane {
@@ -64,6 +86,11 @@ function OutcomeIcon({ outcome }: { readonly outcome: IntelligenceOnboardingOutc
return
}
+function SourceIcon({ group }: { readonly group: IntelligenceOnboardingSourceGroup }) {
+ const Icon = SOURCE_ICONS[group.evidence_role] ?? FileCheck2
+ return
+}
+
function laneState(rank: number, activeAt: number, completeAt: number): BuildState {
if (rank >= completeAt) return 'complete'
if (rank >= activeAt) return 'active'
@@ -120,22 +147,64 @@ function buildLanes(session: IntelligenceBuilderSession | null, watchCount: numb
export function OnboardingPreview({
open,
onOpenChange,
- profile,
+ profiles,
session,
onOpenBrief,
}: {
readonly open: boolean
readonly onOpenChange: (open: boolean) => void
- readonly profile: IntelligenceOnboardingProfile
+ readonly profiles: readonly IntelligenceOnboardingProfile[]
readonly session: IntelligenceBuilderSession | null
readonly onOpenBrief: () => void
}) {
+ const [profileId, setProfileId] = useState(profiles[0].profile_id)
+ const profile = useMemo(
+ () => profiles.find((item) => item.profile_id === profileId) ?? profiles[0],
+ [profileId, profiles],
+ )
const [step, setStep] = useState(0)
+ const [subject, setSubject] = useState(profile.starter_prompts[0] ?? '')
const [outcomeId, setOutcomeId] = useState(profile.outcomes[0]?.outcome_id ?? '')
const [cadenceId, setCadenceId] = useState(profile.default_cadence_id)
+ const [sourceGroupIds, setSourceGroupIds] = useState(() =>
+ profile.source_groups.filter((group) => group.default_selected).map((group) => group.source_group_id),
+ )
const outcome = useMemo(() => profile.outcomes.find((item) => item.outcome_id === outcomeId) ?? profile.outcomes[0], [outcomeId, profile.outcomes])
- const firstBriefReady = session !== null && STAGE_RANK[session.stage] >= STAGE_RANK.first_briefing_ready
- const lanes = buildLanes(session, outcome.recommended_topic_labels.length || 'Custom')
+ const selectedSourceGroups = useMemo(
+ () => profile.source_groups.filter((group) => sourceGroupIds.includes(group.source_group_id)),
+ [profile.source_groups, sourceGroupIds],
+ )
+ const proposedSourceCount = selectedSourceGroups.reduce((total, group) => total + group.source_ids.length, 0)
+ const activeSession = profile.profile_id === profiles[0]?.profile_id ? session : null
+ const firstBriefReady = activeSession !== null && STAGE_RANK[activeSession.stage] >= STAGE_RANK.first_briefing_ready
+ const lanes = buildLanes(activeSession, outcome.recommended_topic_labels.length || 'Custom')
+ const evidenceRequired = profile.source_groups.length > 0
+ const canContinue = step === 0
+ ? subject.trim().length >= 8
+ : step !== 1 || !evidenceRequired || selectedSourceGroups.length > 0
+
+ useEffect(() => {
+ if (!profiles.some((item) => item.profile_id === profileId)) setProfileId(profiles[0].profile_id)
+ }, [profileId, profiles])
+
+ useEffect(() => {
+ if (open) setProfileId(profiles[0].profile_id)
+ }, [open, profiles])
+
+ useEffect(() => {
+ setOutcomeId(profile.outcomes[0]?.outcome_id ?? '')
+ setCadenceId(profile.default_cadence_id)
+ setSubject(profile.starter_prompts[0] ?? '')
+ setSourceGroupIds(
+ profile.source_groups.filter((group) => group.default_selected).map((group) => group.source_group_id),
+ )
+ }, [profile])
+
+ function toggleSourceGroup(sourceGroupId: string) {
+ setSourceGroupIds((current) => current.includes(sourceGroupId)
+ ? current.filter((item) => item !== sourceGroupId)
+ : [...current, sourceGroupId])
+ }
function close(next: boolean) {
onOpenChange(next)
@@ -156,11 +225,16 @@ export function OnboardingPreview({
Build your intelligence
- {session === null ? 'Proposal only' : `Live · step ${session.sequence}`}
+ {activeSession === null ? 'Proposal only' : `Live · step ${activeSession.sequence}`}
-
- {[0, 1, 2, 3].map((item) =>
)}
+
+ {STEP_LABELS.map((label, index) => (
+
+ ))}
@@ -168,9 +242,63 @@ export function OnboardingPreview({
{step === 0 && (
<>
- {profile.prompt}
- {profile.description}
+ What do you want intelligence about?
+
+ Describe the subject or decision in plain language. ACE will choose the strongest available intelligence starting point and specialize it around your job.
+
+
+
+
+
+
Selected intelligence
+
{profile.domain_label} → {profile.topic_label}
+
{profile.display_name} gives ACE the starting vocabulary, evidence roles, and quality policy. Your request determines the actual picture.
+
+
Selected
+
+
+
Choose a starting point
+
+ {profiles.map((item) => {
+ const selected = item.profile_id === profile.profile_id
+ return (
+
setProfileId(item.profile_id)}
+ className={`h-auto min-h-28 w-full flex-col items-start justify-start whitespace-normal rounded-lg border p-4 text-left ${selected ? 'border-brand/70 bg-brand/7' : 'bg-card hover:border-foreground/25 hover:bg-card'}`}
+ >
+
+
{item.domain_label}
+ {selected &&
}
+
+ {item.topic_label}
+ {item.description}
+
+ )
+ })}
+
+
+
{profile.outcomes.map((item) => {
const selected = item.outcome_id === outcomeId
@@ -191,7 +319,58 @@ export function OnboardingPreview({
{step === 1 && (
<>
- Tune the picture
+ Choose the evidence ACE can use
+
+ Start with a balanced public picture. These are proposed source groups—not silent connections—and every record keeps its publisher and evidence role.
+
+
+ {profile.source_groups.length > 0 ? (
+ <>
+
+ {profile.source_groups.map((group) => {
+ const selected = sourceGroupIds.includes(group.source_group_id)
+ return (
+
toggleSourceGroup(group.source_group_id)}
+ className={`h-auto min-h-40 w-full flex-col items-stretch justify-start whitespace-normal rounded-lg border p-4 text-left ${selected ? 'border-brand/70 bg-brand/7' : 'bg-card hover:border-foreground/25 hover:bg-card'}`}
+ >
+
+
+
+
{group.label}{selected && }
+
{group.description}
+
+
+
+ {group.source_labels.slice(0, 4).map((label) => {label} )}
+ {group.source_labels.length > 4 && +{group.source_labels.length - 4} }
+
+
+ {group.source_ids.length} sources {group.access_label}
+
+
+ )
+ })}
+
+
+
{selectedSourceGroups.length} groups · {proposedSourceCount} sources proposed
+
+ >
+ ) : (
+
+ ACE will propose a balanced mix of primary records, first-party claims, independent evidence, telemetry, and leading indicators for review.
+
+ )}
+ >
+ )}
+
+ {step === 2 && (
+ <>
+
+ Shape the intelligence picture
ACE recommends a complete starting view for {outcome.label.toLowerCase()} . You can refine it later.
@@ -201,29 +380,32 @@ export function OnboardingPreview({
>
)}
- {step === 2 && (
+ {step === 3 && (
<>
Review what ACE will build Public evidence creates the first picture. Private sources remain optional and require explicit permission.
-
-
+
0 ? `${proposedSourceCount} proposed sources` : 'Recommended public mix'} detail={selectedSourceGroups.length > 0 ? selectedSourceGroups.map((group) => group.label).join(' · ') : 'Primary records, first-party claims, independent measurement, operational telemetry, and leading indicators.'} />
+ 0 ? outcome.recommended_topic_labels.join(' · ') : 'Entities, aliases, attributes, relationships, claims, events, and outcomes.'} />
- 0 ? outcome.recommended_intelligence_labels.join(' · ') : 'ACE will propose intelligence products from your custom questions.'} />
+ item.cadence_id === cadenceId)?.label ?? 'Selected cadence'} detail={outcome.recommended_intelligence_labels.length > 0 ? outcome.recommended_intelligence_labels.join(' · ') : 'ACE will propose intelligence products from your custom questions.'} />
+
+
+
Nothing is connected or activated silently. You will see every requested permission, every proposed source that remains unconnected, and every watch before it receives authority.
+
First value
One cited Brief
-
Nothing is connected or activated silently. You will see every requested permission, every proposed source that remains unconnected, and every watch before it receives authority.
>
)}
- {step === 3 && (
+ {step === 4 && (
<>
- {firstBriefReady ? 'Your first picture is ready' : session?.stage === 'blocked' ? 'ACE needs your attention' : session === null ? 'Your governed plan is ready' : 'Your first picture is assembling'}
+ {firstBriefReady ? 'Your first picture is ready' : activeSession?.stage === 'blocked' ? 'ACE needs your attention' : activeSession === null ? 'Your governed plan is ready' : 'Your first picture is assembling'}
- {session === null
+ {activeSession === null
? 'Review the plan before ACE connects sources or starts watching.'
: firstBriefReady
? 'ACE built this picture from the sources and watch settings you approved.'
- : session.stage === 'blocked'
+ : activeSession.stage === 'blocked'
? 'ACE paused safely before changing your intelligence picture.'
: 'ACE is assembling the picture from the sources and watch settings you approved.'}
@@ -232,11 +414,11 @@ export function OnboardingPreview({
{lanes.map((lane) => )}
- {session === null
+ {activeSession === null
? 'Reviewing this plan changes nothing until you approve it.'
: firstBriefReady
? 'First cited Brief ready · Setup saved'
- : `Setup saved · Step ${session.sequence}`}
+ : `Setup saved · Step ${activeSession.sequence}`}
>
)}
@@ -244,8 +426,8 @@ export function OnboardingPreview({
setStep((value) => Math.max(0, value - 1))}> Back
- {step < 3
- ?
setStep((value) => Math.min(3, value + 1))}>{step === 2 ? session === null ? 'Review proposed build' : 'View live build' : 'Continue'}
+ {step < 4
+ ?
setStep((value) => Math.min(4, value + 1))}>{step === 0 ? 'Use this starting point' : step === 1 ? 'Use these sources' : step === 2 ? 'Review the plan' : activeSession === null ? 'Review proposed build' : 'View live build'}
:
{firstBriefReady ? profile.completion_label : 'Return to Atrium'} }
diff --git a/core/ui/canvas/src/app/atrium/onboardingModel.test.ts b/core/ui/canvas/src/app/atrium/onboardingModel.test.ts
index 24f58c3..33822a0 100644
--- a/core/ui/canvas/src/app/atrium/onboardingModel.test.ts
+++ b/core/ui/canvas/src/app/atrium/onboardingModel.test.ts
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest'
import type { IntelligenceResourceRecord } from '@/api/intelligenceResourcesApi'
import {
onboardingProfileFromResources,
+ onboardingProfilesFromResources,
onboardingSessionFromResources,
hasOnboardingProfileResource,
parseBuilderSession,
@@ -14,8 +15,11 @@ const profile = {
profile_id: 'onboarding_profile:test',
topic_id: 'test-topic',
display_name: 'Test Intelligence',
+ domain_label: 'Test domain',
+ topic_label: 'Testing',
prompt: 'What matters?',
description: 'Choose an outcome.',
+ starter_prompts: ['Keep me ahead of meaningful test changes.'],
outcomes: [{
outcome_id: 'test',
label: 'Track the test',
@@ -24,6 +28,16 @@ const profile = {
recommended_topic_labels: ['Tests'],
recommended_intelligence_labels: ['Test movement'],
}],
+ source_groups: [{
+ source_group_id: 'public_records',
+ label: 'Public records',
+ description: 'Primary evidence for the test.',
+ evidence_role: 'authoritative_record',
+ source_ids: ['test_registry'],
+ source_labels: ['Test Registry'],
+ access_label: 'Public · no credentials',
+ default_selected: true,
+ }],
cadences: [{ cadence_id: 'weekly', label: 'Weekly', description: 'Once a week.' }],
default_cadence_id: 'weekly',
first_value: { completion_label: 'Open the test brief' },
@@ -63,13 +77,19 @@ describe('Atrium onboarding resources', () => {
it('accepts the Core-owned declarative profile contract', () => {
expect(parseOnboardingProfile(profile)).toMatchObject({
display_name: 'Test Intelligence',
+ domain_label: 'Test domain',
+ topic_label: 'Testing',
+ starter_prompts: ['Keep me ahead of meaningful test changes.'],
completion_label: 'Open the test brief',
+ source_groups: [{ source_group_id: 'public_records', default_selected: true }],
})
})
it('rejects unknown or malformed payloads', () => {
expect(parseOnboardingProfile({ ...profile, contract: 'unknown' })).toBeNull()
expect(parseOnboardingProfile({ ...profile, outcomes: [] })).toBeNull()
+ expect(parseOnboardingProfile({ ...profile, starter_prompts: [3] })).toBeNull()
+ expect(parseOnboardingProfile({ ...profile, source_groups: [{ ...profile.source_groups[0], source_ids: [] }] })).toBeNull()
expect(parseBuilderSession({ ...session, stage: 'invented' })).toBeNull()
})
@@ -77,10 +97,20 @@ describe('Atrium onboarding resources', () => {
expect(onboardingProfileFromResources([resource('builder_profile', profile)]).display_name).toBe('Test Intelligence')
expect(hasOnboardingProfileResource([resource('builder_profile', profile)])).toBe(true)
expect(hasOnboardingProfileResource([])).toBe(false)
- expect(onboardingProfileFromResources([]).display_name).toBe('Your Intelligence')
+ expect(onboardingProfileFromResources([]).display_name).toBe('Custom Intelligence')
expect(onboardingProfileFromResources([]).outcomes.some((item) => item.label.includes('AI'))).toBe(false)
})
+ it('builds a domain-neutral catalog and always offers the custom Builder path', () => {
+ const market = { ...profile, profile_id: 'onboarding_profile:market', domain_label: 'Market Intelligence' }
+ expect(onboardingProfilesFromResources([
+ resource('builder_profile', profile),
+ resource('builder_profile', market, 2),
+ resource('builder_profile', profile, 3),
+ ]).map((item) => item.domain_label)).toEqual(['Test domain', 'Market Intelligence', 'Custom Intelligence'])
+ expect(onboardingProfilesFromResources([]).map((item) => item.domain_label)).toEqual(['Custom Intelligence'])
+ })
+
it('uses the latest exact builder session revision', () => {
const older = resource('builder_session', { ...session, sequence: 6, stage: 'intelligence_model_approved' }, 6)
const latest = resource('builder_session', session, 7)
diff --git a/core/ui/canvas/src/app/atrium/onboardingModel.ts b/core/ui/canvas/src/app/atrium/onboardingModel.ts
index fcce13a..620f3ab 100644
--- a/core/ui/canvas/src/app/atrium/onboardingModel.ts
+++ b/core/ui/canvas/src/app/atrium/onboardingModel.ts
@@ -18,12 +18,29 @@ export interface IntelligenceOnboardingCadence {
readonly description: string
}
+export interface IntelligenceOnboardingSourceGroup {
+ readonly source_group_id: string
+ readonly label: string
+ readonly description: string
+ readonly evidence_role: string
+ readonly source_ids: readonly string[]
+ readonly source_labels: readonly string[]
+ readonly access_label: string
+ readonly default_selected: boolean
+}
+
export interface IntelligenceOnboardingProfile {
readonly contract: typeof PROFILE_CONTRACT | typeof LEGACY_PROFILE_CONTRACT
+ readonly profile_id: string
+ readonly topic_id: string
+ readonly domain_label: string
+ readonly topic_label: string
readonly display_name: string
readonly prompt: string
readonly description: string
+ readonly starter_prompts: readonly string[]
readonly outcomes: readonly IntelligenceOnboardingOutcome[]
+ readonly source_groups: readonly IntelligenceOnboardingSourceGroup[]
readonly cadences: readonly IntelligenceOnboardingCadence[]
readonly default_cadence_id: string
readonly completion_label: string
@@ -62,9 +79,16 @@ export interface IntelligenceBuilderSession {
const FALLBACK_PROFILE: IntelligenceOnboardingProfile = {
contract: PROFILE_CONTRACT,
- display_name: 'Your Intelligence',
+ profile_id: 'onboarding_profile:custom-intelligence',
+ topic_id: 'custom_intelligence',
+ domain_label: 'Custom Intelligence',
+ topic_label: 'Built around your question',
+ display_name: 'Custom Intelligence',
prompt: 'What do you need to stay ahead of?',
description: 'Choose the decision context. ACE will recommend the evidence, concepts, watches, and briefing system.',
+ starter_prompts: [
+ 'Help me stay ahead of the changes that could materially affect my decisions.',
+ ],
outcomes: [
{
outcome_id: 'choice',
@@ -115,6 +139,7 @@ const FALLBACK_PROFILE: IntelligenceOnboardingProfile = {
recommended_intelligence_labels: [],
},
],
+ source_groups: [],
cadences: [
{ cadence_id: 'urgent', label: 'Urgent only', description: 'Only material thresholds, contradictions, and incidents.' },
{ cadence_id: 'daily', label: 'Daily pulse', description: 'A concise daily orientation plus urgent alerts.' },
@@ -181,6 +206,28 @@ function parseCadence(value: unknown): IntelligenceOnboardingCadence | null {
return { cadence_id: value.cadence_id, label: value.label, description: value.description }
}
+function parseSourceGroup(value: unknown): IntelligenceOnboardingSourceGroup | null {
+ if (!isRecord(value)) return null
+ const sourceIds = strings(value.source_ids)
+ const sourceLabels = strings(value.source_labels)
+ if (
+ typeof value.source_group_id !== 'string' || typeof value.label !== 'string' ||
+ typeof value.description !== 'string' || typeof value.evidence_role !== 'string' ||
+ sourceIds === null || sourceIds.length === 0 || sourceLabels === null || sourceLabels.length === 0 ||
+ typeof value.access_label !== 'string' || typeof value.default_selected !== 'boolean'
+ ) return null
+ return {
+ source_group_id: value.source_group_id,
+ label: value.label,
+ description: value.description,
+ evidence_role: value.evidence_role,
+ source_ids: sourceIds,
+ source_labels: sourceLabels,
+ access_label: value.access_label,
+ default_selected: value.default_selected,
+ }
+}
+
export function parseOnboardingProfile(value: unknown): IntelligenceOnboardingProfile | null {
if (!isRecord(value) || (value.contract !== PROFILE_CONTRACT && value.contract !== LEGACY_PROFILE_CONTRACT)) return null
if (
@@ -188,24 +235,61 @@ export function parseOnboardingProfile(value: unknown): IntelligenceOnboardingPr
typeof value.description !== 'string' || typeof value.default_cadence_id !== 'string'
) return null
const outcomes = Array.isArray(value.outcomes) ? value.outcomes.map(parseOutcome) : []
+ const sourceGroups = Array.isArray(value.source_groups) ? value.source_groups.map(parseSourceGroup) : []
const cadences = Array.isArray(value.cadences) ? value.cadences.map(parseCadence) : []
- if (outcomes.length === 0 || outcomes.some((item) => item === null) || cadences.length === 0 || cadences.some((item) => item === null)) return null
+ if (
+ outcomes.length === 0 || outcomes.some((item) => item === null) ||
+ sourceGroups.some((item) => item === null) ||
+ cadences.length === 0 || cadences.some((item) => item === null)
+ ) return null
const firstValue = isRecord(value.first_value) ? value.first_value : null
+ const starterPrompts = value.starter_prompts === undefined ? [] : strings(value.starter_prompts)
+ if (starterPrompts === null) return null
const completionLabel = firstValue !== null && typeof firstValue.completion_label === 'string'
? firstValue.completion_label
: 'Open my first briefing'
return {
contract: value.contract,
+ profile_id: typeof value.profile_id === 'string' ? value.profile_id : `onboarding_profile:${value.display_name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
+ topic_id: typeof value.topic_id === 'string' ? value.topic_id : value.display_name.toLowerCase().replace(/[^a-z0-9]+/g, '_'),
+ domain_label: typeof value.domain_label === 'string' ? value.domain_label : 'Installed intelligence',
+ topic_label: typeof value.topic_label === 'string' ? value.topic_label : value.display_name,
display_name: value.display_name,
prompt: value.prompt,
description: value.description,
+ starter_prompts: starterPrompts,
outcomes: outcomes as IntelligenceOnboardingOutcome[],
+ source_groups: sourceGroups as IntelligenceOnboardingSourceGroup[],
cadences: cadences as IntelligenceOnboardingCadence[],
default_cadence_id: value.default_cadence_id,
completion_label: completionLabel,
}
}
+/** All admitted starting points visible in the current product scope.
+ *
+ * Domain profiles are contributed through Builder profile resources. Custom
+ * Intelligence is the one Core-owned starting point because it invokes the
+ * generic discovery agents rather than naming a domain. Exact duplicates are
+ * collapsed by profile identity; no domain name is hard-coded here.
+ */
+export function onboardingProfilesFromResources(items: readonly IntelligenceResourceRecord[]): readonly IntelligenceOnboardingProfile[] {
+ const profiles = new Map
()
+ for (const item of items) {
+ const payload = canonicalPayload(item.payload)
+ const candidates = item.reference.resource_kind === 'builder_profile'
+ ? [parseOnboardingProfile(payload)]
+ : item.reference.resource_kind === 'context_manifest' && isRecord(payload)
+ ? [parseOnboardingProfile(payload.onboarding_profile)]
+ : []
+ for (const profile of candidates) {
+ if (profile !== null && !profiles.has(profile.profile_id)) profiles.set(profile.profile_id, profile)
+ }
+ }
+ profiles.set(FALLBACK_PROFILE.profile_id, FALLBACK_PROFILE)
+ return [...profiles.values()]
+}
+
function parseBuilderArtifact(value: unknown): IntelligenceBuilderArtifact | null {
if (!isRecord(value)) return null
if (typeof value.artifact_kind !== 'string' || typeof value.artifact_id !== 'string' || typeof value.artifact_digest !== 'string') return null
@@ -245,18 +329,7 @@ export function parseBuilderSession(value: unknown): IntelligenceBuilderSession
}
export function onboardingProfileFromResources(items: readonly IntelligenceResourceRecord[]): IntelligenceOnboardingProfile {
- for (const item of items) {
- const payload = canonicalPayload(item.payload)
- if (item.reference.resource_kind === 'builder_profile') {
- const profile = parseOnboardingProfile(payload)
- if (profile !== null) return profile
- }
- if (item.reference.resource_kind === 'context_manifest' && isRecord(payload)) {
- const profile = parseOnboardingProfile(payload.onboarding_profile)
- if (profile !== null) return profile
- }
- }
- return FALLBACK_PROFILE
+ return onboardingProfilesFromResources(items).find((profile) => profile.profile_id !== FALLBACK_PROFILE.profile_id) ?? FALLBACK_PROFILE
}
export function onboardingSessionFromResources(items: readonly IntelligenceResourceRecord[]): IntelligenceBuilderSession | null {
diff --git a/core/ui/canvas/tests/e2e/atrium-intelligence-os.spec.ts b/core/ui/canvas/tests/e2e/atrium-intelligence-os.spec.ts
index 4cf9083..10d9dfc 100644
--- a/core/ui/canvas/tests/e2e/atrium-intelligence-os.spec.ts
+++ b/core/ui/canvas/tests/e2e/atrium-intelligence-os.spec.ts
@@ -96,7 +96,7 @@ test('Atrium is a briefing-first Intelligence OS over governed resources', async
await page.getByLabel('Ask ACE about current intelligence').fill('What changed in token economics?')
await page.getByLabel('Ask ACE', { exact: true }).click()
await expect(page.getByText('Frontier inference costs moved down again').first()).toBeVisible()
- await expect(page.getByText(/cited record/)).toBeVisible()
+ await expect(page.getByText(/cited record/).first()).toBeVisible()
await page.getByText('Opportunities', { exact: true }).click()
await expect(page.getByRole('heading', { name: 'Opportunities' })).toBeVisible()
@@ -116,13 +116,20 @@ test('Atrium is a briefing-first Intelligence OS over governed resources', async
test('Atrium empty state starts with the user job and previews a complete first-Brief journey', async ({ page }, testInfo) => {
const onboardingProfile = {
contract: 'ace.domain-pack.intelligence-onboarding-profile/v1alpha1',
+ domain_label: 'World Intelligence',
+ topic_label: 'Artificial intelligence',
display_name: 'AI Command Center',
prompt: 'What do you need to stay ahead of?',
description: 'Choose the AI decision context. ACE recommends the sources, concepts, watches, and briefing system.',
+ starter_prompts: ['Keep me ahead of meaningful AI capability, cost, policy, and adoption shifts.'],
outcomes: [
{ outcome_id: 'strategy', label: 'Set strategy or evaluate investments', description: 'See which capital and capability moves are becoming durable advantage.', icon_hint: 'strategy', recommended_topic_labels: ['Capital', 'Capabilities'], recommended_intelligence_labels: ['Capital-to-capability'] },
{ outcome_id: 'frontier', label: 'Track frontier research and products', description: 'Follow advances into evaluated products.', icon_hint: 'research', recommended_topic_labels: ['Open research', 'Models & capabilities'], recommended_intelligence_labels: ['Research-to-product diffusion'] },
],
+ source_groups: [
+ { source_group_id: 'official_records', label: 'Official records', description: 'Policy, filings, and authoritative publications.', evidence_role: 'authoritative_record', source_ids: ['federal_register', 'sec_edgar'], source_labels: ['Federal Register', 'SEC EDGAR'], access_label: 'Public · no credentials', default_selected: true },
+ { source_group_id: 'open_ecosystem', label: 'Open ecosystem', description: 'Research and repository movement.', evidence_role: 'leading_indicator', source_ids: ['arxiv', 'github'], source_labels: ['arXiv', 'GitHub'], access_label: 'Public · optional token', default_selected: true },
+ ],
cadences: [
{ cadence_id: 'daily', label: 'Daily pulse', description: 'A concise daily orientation.' },
{ cadence_id: 'weekly', label: 'Weekly briefing', description: "The week's movement." },
@@ -134,6 +141,24 @@ test('Atrium empty state starts with the user job and previews a complete first-
...resource('context_manifest', 'world-ai-onboarding', 'AI intelligence setup', 'The reviewed first-run profile.'),
payload: { onboarding_profile: onboardingProfile },
}
+ const marketProfile = {
+ ...onboardingProfile,
+ contract: 'ace.intelligence.onboarding-profile/v1alpha1',
+ profile_id: 'onboarding_profile:market-intelligence',
+ topic_id: 'market_intelligence',
+ domain_label: 'Marketing Intelligence',
+ topic_label: 'Your market and competitors',
+ display_name: 'Marketing Intelligence Command Center',
+ description: 'Understand markets, competitors, customers, products, and go-to-market movement.',
+ starter_prompts: ['Keep me ahead of competitor, customer, product, and market shifts.'],
+ }
+ const marketProfileResource = {
+ ...resource('builder_profile', 'market-intelligence', 'Marketing Intelligence', 'A governed commercial starting point.'),
+ payload: {
+ contract: 'ace.intelligence.canonical-json-value/v1alpha1',
+ value_json: JSON.stringify(marketProfile),
+ },
+ }
await page.route('**/auth/token', (route) =>
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ token: 'test-token' }) }),
)
@@ -151,7 +176,7 @@ test('Atrium empty state starts with the user job and previews a complete first-
available_at: availableAt,
evaluated_at: availableAt,
state: 'complete',
- items: [contextManifest],
+ items: [contextManifest, marketProfileResource],
next_cursor: null,
degraded_reason_refs: [],
page_id: 'resource_page:empty',
@@ -164,17 +189,30 @@ test('Atrium empty state starts with the user job and previews a complete first-
await expect(page.getByRole('heading', { name: 'What do you need to stay ahead of?' })).toBeVisible()
await page.getByRole('button', { name: 'Build my intelligence' }).click()
- await expect(page.getByRole('heading', { name: 'What do you need to stay ahead of?' })).toBeVisible()
+ await expect(page.getByRole('heading', { name: 'What do you want intelligence about?' })).toBeVisible()
+ await expect(page.getByRole('button', { name: /World Intelligence/ })).toBeVisible()
+ await expect(page.getByRole('button', { name: /Marketing Intelligence/ })).toBeVisible()
+ await expect(page.getByRole('button', { name: /Custom Intelligence/ })).toBeVisible()
+ await page.getByRole('button', { name: /World Intelligence/ }).click()
if (process.env.ACE_CAPTURE_ATRIUM === '1') {
await page.screenshot({ path: testInfo.outputPath('atrium-onboarding-outcome.png'), fullPage: true })
}
await page.getByRole('button', { name: /Track frontier research and products/ }).click()
- await page.getByRole('button', { name: 'Continue' }).click()
- await expect(page.getByRole('heading', { name: 'Tune the picture' })).toBeVisible()
+ await page.getByRole('button', { name: 'Use this starting point' }).click()
+ await expect(page.getByRole('heading', { name: 'Choose the evidence ACE can use' })).toBeVisible()
+ await expect(page.getByText('2 groups · 4 sources proposed')).toBeVisible()
+ if (process.env.ACE_CAPTURE_ATRIUM === '1') {
+ await page.screenshot({ path: testInfo.outputPath('atrium-onboarding-evidence.png'), fullPage: true })
+ }
+ await page.getByRole('button', { name: 'Use these sources' }).click()
+ await expect(page.getByRole('heading', { name: 'Shape the intelligence picture' })).toBeVisible()
await page.getByRole('button', { name: 'Daily pulse' }).click()
- await page.getByRole('button', { name: 'Continue' }).click()
+ await page.getByRole('button', { name: 'Review the plan' }).click()
await expect(page.getByRole('heading', { name: 'Review what ACE will build' })).toBeVisible()
await expect(page.getByText('Nothing is connected or activated silently.')).toBeVisible()
+ if (process.env.ACE_CAPTURE_ATRIUM === '1') {
+ await page.screenshot({ path: testInfo.outputPath('atrium-onboarding-review.png'), fullPage: true })
+ }
await page.getByRole('button', { name: 'Review proposed build' }).click()
await expect(page.getByRole('heading', { name: 'Your governed plan is ready' })).toBeVisible()
await expect(page.getByText('Proposed', { exact: true })).toHaveCount(4)
@@ -187,9 +225,12 @@ test('Atrium renders a durable first-brief-ready Builder session instead of simu
contract: 'ace.intelligence.onboarding-profile/v1alpha1',
profile_id: 'intelligence_onboarding_profile:world-ai-command-center',
topic_id: 'artificial_intelligence',
+ domain_label: 'World Intelligence',
+ topic_label: 'Artificial intelligence',
display_name: 'AI Command Center',
prompt: 'What do you need to stay ahead of?',
description: 'Build an evidence-grounded picture of the AI landscape.',
+ starter_prompts: ['Keep me ahead of material AI policy, capability, and adoption changes.'],
outcomes: [{
outcome_id: 'strategy',
label: 'Set strategy or evaluate investments',
@@ -198,6 +239,16 @@ test('Atrium renders a durable first-brief-ready Builder session instead of simu
recommended_topic_labels: ['Policy', 'Models'],
recommended_intelligence_labels: ['Policy progression'],
}],
+ source_groups: [{
+ source_group_id: 'official_records',
+ label: 'Official records',
+ description: 'Primary policy evidence.',
+ evidence_role: 'authoritative_record',
+ source_ids: ['federal_register', 'white_house'],
+ source_labels: ['Federal Register', 'White House'],
+ access_label: 'Public · no credentials',
+ default_selected: true,
+ }],
cadences: [{ cadence_id: 'daily', label: 'Daily pulse', description: 'A concise daily orientation.' }],
default_cadence_id: 'daily',
first_value: { completion_label: 'Open my first briefing' },
@@ -284,8 +335,9 @@ test('Atrium renders a durable first-brief-ready Builder session instead of simu
await page.goto('/atrium')
await page.getByRole('button', { name: 'View build' }).click()
- await page.getByRole('button', { name: 'Continue' }).click()
- await page.getByRole('button', { name: 'Continue' }).click()
+ await page.getByRole('button', { name: 'Use this starting point' }).click()
+ await page.getByRole('button', { name: 'Use these sources' }).click()
+ await page.getByRole('button', { name: 'Review the plan' }).click()
await page.getByRole('button', { name: 'View live build' }).click()
await expect(page.getByRole('heading', { name: 'Your first picture is ready' })).toBeVisible()
await expect(page.getByText('Complete', { exact: true })).toHaveCount(4)
diff --git a/docs/design/atrium-jtbd-onboarding-reference-lock-v1.md b/docs/design/atrium-jtbd-onboarding-reference-lock-v1.md
index ff91bda..cccf8ee 100644
--- a/docs/design/atrium-jtbd-onboarding-reference-lock-v1.md
+++ b/docs/design/atrium-jtbd-onboarding-reference-lock-v1.md
@@ -30,15 +30,38 @@ serve it.
| Decide | What opportunity, risk, or investigation follows? | A bounded next step into Opportunities, Strategy, or downstream investigation. |
| Learn | Can this become more useful without silently changing truth or authority? | Useful/not-useful feedback reweights relevance; authority, evidence, and policy do not self-widen. |
+## Intelligence Catalog
+
+Atrium opens by asking **What do you want intelligence about?** It then presents the admitted
+starting points available in the current product scope. The initial public catalog is:
+
+- **World Intelligence** — topic-centric orientation over public-world change; the flagship topic
+ is artificial intelligence.
+- **Marketing Intelligence** — commercial orientation over markets, competitors, products,
+ customers, narratives, go-to-market movement, and marketing performance.
+- **Custom Intelligence** — the Core-owned generic Builder path for a subject that does not match
+ an admitted domain profile.
+
+World and Marketing are not constants in the Core kernel or conditional UI branches. Their owning
+repositories contribute inert `builder_profile` resources through the existing resource plane.
+Atrium deduplicates profiles by stable identity and appends Custom Intelligence as its sole built-in
+starting point. A missing or uninstalled domain is not advertised as available.
+
+The catalog is presentation and discovery, not installation or authority. Selecting a card changes
+the proposed vocabulary, evidence roles, outcomes, and cadence; it does not connect a source, create
+a monitor, activate a pack, or bind a session. A product host begins live work only through the
+public Builder and approval boundaries.
+
## First-run journey
The magic moment is `choose a job -> accept a recommended watch system -> see the first cited
briefing`. Public evidence can produce first value before the user connects private data.
-### 1. Choose the outcome
+### 1. State the job and choose the starting point
-The first screen asks one question: **What do you need to stay ahead of?** A Domain Pack supplies a
-small set of outcome choices. The World AI pack starts with:
+The first screen accepts the user's subject or decision in plain language, recommends an admitted
+starting point, and lets the user choose another. The selected profile then supplies a small set of
+outcome choices. The World AI pack starts with:
- choose or buy AI;
- set strategy or evaluate investments;
@@ -182,6 +205,12 @@ The design direction was researched before implementation:
than decorative agent animation.
- Reclaim contributes the sequence `connect -> confirm -> personalize -> provision`, with setup
status retained after the user leaves the first-run flow.
+- Cycle contributes a compact source-selection grid with enough publisher detail to make a choice
+ without becoming an integration wall.
+- Sana contributes the explicit connect/continue boundary: a source remains visibly proposed until
+ access succeeds and the user continues.
+- Dovetail contributes a dark, information-dense research workspace that keeps evidence and the
+ user's question visually primary.
- Macaw contributes the immediate handoff from inspected source understanding to a populated
generated result rather than a blank success screen.
- Gemini contributes the centered readable answer with a dedicated source panel one interaction
@@ -215,14 +244,16 @@ states; the interface must not average them into a single optimistic progress tr
## Acceptance journey
1. A clean install opens with no live intelligence and offers one outcome-led start action.
-2. A user selects an AI decision context, accepts recommended topics and cadence, and sees proposed
+2. Atrium lists every admitted domain starting point plus Custom Intelligence without naming an
+ unavailable domain or granting installation authority.
+3. A user states the job, selects an AI decision context, accepts recommended topics and cadence, and sees proposed
public evidence before granting authority.
-3. ACE explains every requested permission and keeps failed or skipped connections resumable.
-4. The governed agents produce an inspectable Connect -> Map -> Watch -> Brief -> Activate trace.
-5. The first cited Brief appears without hand-authored ontology work or knowledge of ACE internals.
-6. The user can ask a grounded question, inspect exact evidence, mark relevance, and see that
+4. ACE explains every requested permission and keeps failed or skipped connections resumable.
+5. The governed agents produce an inspectable Connect -> Map -> Watch -> Brief -> Activate trace.
+6. The first cited Brief appears without hand-authored ontology work or knowledge of ACE internals.
+7. The user can ask a grounded question, inspect exact evidence, mark relevance, and see that
feedback changes ranking without changing evidence or authority.
-7. Restart/reopen returns to the same active watches, latest Brief, onboarding history, and user
+8. Restart/reopen returns to the same active watches, latest Brief, onboarding history, and user
preferences.
-8. The same Core shell reproduces a materially different Market Intelligence profile without code
+9. The same Core shell reproduces a materially different Market Intelligence profile without code
changes.
diff --git a/tests/intelligence/test_intelligence_builder_resource_projection.py b/tests/intelligence/test_intelligence_builder_resource_projection.py
index 9204183..71f7271 100644
--- a/tests/intelligence/test_intelligence_builder_resource_projection.py
+++ b/tests/intelligence/test_intelligence_builder_resource_projection.py
@@ -19,6 +19,7 @@
IntelligenceOnboardingGuardrailsV1Alpha1,
IntelligenceOnboardingOutcomeV1Alpha1,
IntelligenceOnboardingProfileV1Alpha1,
+ IntelligenceOnboardingSourceGroupV1Alpha1,
IntelligenceResourceAvailability,
IntelligenceResourceKind,
IntelligenceResourcePageState,
@@ -36,9 +37,12 @@ def _profile() -> IntelligenceOnboardingProfileV1Alpha1:
return IntelligenceOnboardingProfileV1Alpha1(
profile_id="onboarding_profile:ai-command-center",
topic_id="artificial-intelligence",
+ domain_label="World Intelligence",
+ topic_label="Artificial intelligence",
display_name="AI Command Center",
prompt="What should your AI command center help you understand?",
description="Connect authoritative AI sources and receive a grounded first briefing.",
+ starter_prompts=("Keep me ahead of meaningful AI capability, cost, and policy shifts.",),
outcomes=(
IntelligenceOnboardingOutcomeV1Alpha1(
outcome_id="track-model-economics",
@@ -51,6 +55,18 @@ def _profile() -> IntelligenceOnboardingProfileV1Alpha1:
recommended_intelligence_labels=("Market shifts",),
),
),
+ source_groups=(
+ IntelligenceOnboardingSourceGroupV1Alpha1(
+ source_group_id="independent-evidence",
+ label="Independent evidence",
+ description="Reviewed measurements that test first-party claims.",
+ evidence_role="independent-measurement",
+ source_ids=("stanford-helm", "metr"),
+ source_labels=("Stanford HELM", "METR"),
+ access_label="Public · no credentials",
+ default_selected=True,
+ ),
+ ),
cadences=(
IntelligenceOnboardingCadenceV1Alpha1(
cadence_id="daily",
@@ -85,6 +101,43 @@ def _query(*kinds: IntelligenceResourceKind) -> IntelligenceResourceQueryV1Alpha
)
+@pytest.mark.asyncio
+async def test_multiple_domain_profiles_form_one_product_scoped_catalog() -> None:
+ store = InMemoryImmutableRecordStore()
+ service = IntelligenceBuilderPresentationService(store=store)
+ world = _profile()
+ market_payload = world.model_dump(mode="python")
+ market_payload.update(
+ {
+ "profile_id": "onboarding_profile:market-intelligence",
+ "profile_digest": None,
+ "topic_id": "market-intelligence",
+ "domain_label": "Marketing Intelligence",
+ "topic_label": "Your market and competitors",
+ "display_name": "Marketing Intelligence Command Center",
+ }
+ )
+ market = IntelligenceOnboardingProfileV1Alpha1.model_validate(market_payload)
+
+ await service.admit_profile(product_id=PRODUCT, profile=world, admitted_at=BASE)
+ await service.admit_profile(product_id=PRODUCT, profile=market, admitted_at=BASE + timedelta(seconds=1))
+
+ batch = await IntelligenceBuilderResourceProjectionReader(store=store).read(
+ query=_query(IntelligenceResourceKind.BUILDER_PROFILE),
+ after=None,
+ limit=30,
+ )
+
+ assert [record.reference.resource_id for record in batch.records] == [
+ "onboarding_profile:ai-command-center",
+ "onboarding_profile:market-intelligence",
+ ]
+ assert [record.payload.parsed_value()["domain_label"] for record in batch.records if record.payload] == [
+ "World Intelligence",
+ "Marketing Intelligence",
+ ]
+
+
@pytest.mark.asyncio
async def test_profile_and_session_revisions_project_through_one_rebuildable_reader() -> None:
store = InMemoryImmutableRecordStore()
@@ -136,6 +189,23 @@ async def test_profile_and_session_revisions_project_through_one_rebuildable_rea
profile_record, first_session, second_session = batch.records
assert profile_record.payload is not None
assert profile_record.payload.parsed_value()["guardrails"]["authorizes_connections"] is False
+ assert profile_record.payload.parsed_value()["domain_label"] == "World Intelligence"
+ assert profile_record.payload.parsed_value()["topic_label"] == "Artificial intelligence"
+ assert profile_record.payload.parsed_value()["starter_prompts"] == [
+ "Keep me ahead of meaningful AI capability, cost, and policy shifts."
+ ]
+ assert profile_record.payload.parsed_value()["source_groups"] == [
+ {
+ "access_label": "Public · no credentials",
+ "default_selected": True,
+ "description": "Reviewed measurements that test first-party claims.",
+ "evidence_role": "independent-measurement",
+ "label": "Independent evidence",
+ "source_group_id": "independent-evidence",
+ "source_ids": ["metr", "stanford-helm"],
+ "source_labels": ["METR", "Stanford HELM"],
+ }
+ ]
assert first_session.reference.revision == 1
assert second_session.reference.revision == 2
assert second_session.supersedes == first_session.reference