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 && ( - - )} + {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. + +
+