productfoo 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +338 -2
- package/bin/foo +3 -0
- package/bin/foo.mjs +42 -0
- package/commands/bug.md +31 -0
- package/commands/discovery.md +28 -0
- package/commands/ideate.md +27 -0
- package/commands/init.md +28 -0
- package/commands/pitch.md +27 -0
- package/commands/preview.md +25 -0
- package/commands/research.md +31 -0
- package/commands/roast.md +27 -0
- package/commands/spec.md +30 -0
- package/config/pricing.yaml +198 -0
- package/config/routing-policy.yaml +171 -0
- package/dist-public/src/activeModel.d.ts +83 -0
- package/dist-public/src/activeModel.js +331 -0
- package/dist-public/src/agentAdapters.d.ts +122 -0
- package/dist-public/src/agentAdapters.js +178 -0
- package/dist-public/src/agentState.d.ts +43 -0
- package/dist-public/src/agentState.js +177 -0
- package/dist-public/src/bugCapsule.d.ts +94 -0
- package/dist-public/src/bugCapsule.js +227 -0
- package/dist-public/src/bugCapture.d.ts +26 -0
- package/dist-public/src/bugCapture.js +251 -0
- package/dist-public/src/bugOwnerLog.d.ts +93 -0
- package/dist-public/src/bugOwnerLog.js +304 -0
- package/dist-public/src/bugReportStore.d.ts +48 -0
- package/dist-public/src/bugReportStore.js +860 -0
- package/dist-public/src/bugSpecContext.d.ts +7 -0
- package/dist-public/src/bugSpecContext.js +129 -0
- package/dist-public/src/channelFreshness.d.ts +25 -0
- package/dist-public/src/channelFreshness.js +194 -0
- package/dist-public/src/cli.d.ts +30 -0
- package/dist-public/src/cli.js +1667 -0
- package/dist-public/src/cliArgs.d.ts +7 -0
- package/dist-public/src/cliArgs.js +44 -0
- package/dist-public/src/codeCatalog.d.ts +41 -0
- package/dist-public/src/codeCatalog.js +197 -0
- package/dist-public/src/commandContract.d.ts +84 -0
- package/dist-public/src/commandContract.js +390 -0
- package/dist-public/src/commandUsage.d.ts +3 -0
- package/dist-public/src/commandUsage.js +30 -0
- package/dist-public/src/commands/agent.d.ts +60 -0
- package/dist-public/src/commands/agent.js +1353 -0
- package/dist-public/src/commands/asp.d.ts +8 -0
- package/dist-public/src/commands/asp.js +669 -0
- package/dist-public/src/commands/evalCmd.d.ts +4 -0
- package/dist-public/src/commands/evalCmd.js +405 -0
- package/dist-public/src/commands/login.d.ts +25 -0
- package/dist-public/src/commands/login.js +94 -0
- package/dist-public/src/commands/loop.d.ts +16 -0
- package/dist-public/src/commands/loop.js +619 -0
- package/dist-public/src/commands/reconcile.d.ts +41 -0
- package/dist-public/src/commands/reconcile.js +292 -0
- package/dist-public/src/commands/runtime.d.ts +1 -0
- package/dist-public/src/commands/runtime.js +36 -0
- package/dist-public/src/commands/scanRedact.d.ts +8 -0
- package/dist-public/src/commands/scanRedact.js +266 -0
- package/dist-public/src/commands/setup.d.ts +28 -0
- package/dist-public/src/commands/setup.js +398 -0
- package/dist-public/src/commands/shared.d.ts +17 -0
- package/dist-public/src/commands/shared.js +147 -0
- package/dist-public/src/commands/source.d.ts +1 -0
- package/dist-public/src/commands/source.js +134 -0
- package/dist-public/src/commands/specCmd.d.ts +24 -0
- package/dist-public/src/commands/specCmd.js +794 -0
- package/dist-public/src/commands/update.d.ts +61 -0
- package/dist-public/src/commands/update.js +244 -0
- package/dist-public/src/comprehensionSignal.d.ts +20 -0
- package/dist-public/src/comprehensionSignal.js +166 -0
- package/dist-public/src/conductor/diversity.d.ts +48 -0
- package/dist-public/src/conductor/diversity.js +423 -0
- package/dist-public/src/conductor/engine.d.ts +7 -0
- package/dist-public/src/conductor/engine.js +295 -0
- package/dist-public/src/conductor/families/ideate.d.ts +2 -0
- package/dist-public/src/conductor/families/ideate.js +255 -0
- package/dist-public/src/conductor/families/interview.d.ts +2 -0
- package/dist-public/src/conductor/families/interview.js +39 -0
- package/dist-public/src/conductor/families/specSwarm.d.ts +2 -0
- package/dist-public/src/conductor/families/specSwarm.js +146 -0
- package/dist-public/src/conductor/familyDelivery.d.ts +6 -0
- package/dist-public/src/conductor/familyDelivery.js +37 -0
- package/dist-public/src/conductor/familyPackArtifact.d.ts +4 -0
- package/dist-public/src/conductor/familyPackArtifact.js +260 -0
- package/dist-public/src/conductor/ideateDecision.d.ts +6 -0
- package/dist-public/src/conductor/ideateDecision.js +237 -0
- package/dist-public/src/conductor/pack.d.ts +26 -0
- package/dist-public/src/conductor/pack.js +75 -0
- package/dist-public/src/conductor/types.d.ts +111 -0
- package/dist-public/src/conductor/types.js +7 -0
- package/dist-public/src/configPath.d.ts +2 -0
- package/dist-public/src/configPath.js +7 -0
- package/dist-public/src/discoveryArtifacts.d.ts +54 -0
- package/dist-public/src/discoveryArtifacts.js +467 -0
- package/dist-public/src/discoveryCommand.d.ts +127 -0
- package/dist-public/src/discoveryCommand.js +2148 -0
- package/dist-public/src/discoveryConnections.d.ts +339 -0
- package/dist-public/src/discoveryConnections.js +613 -0
- package/dist-public/src/discoveryDocs.d.ts +129 -0
- package/dist-public/src/discoveryDocs.js +577 -0
- package/dist-public/src/discoveryEvidence.d.ts +59 -0
- package/dist-public/src/discoveryEvidence.js +198 -0
- package/dist-public/src/discoveryFireflies.d.ts +25 -0
- package/dist-public/src/discoveryFireflies.js +551 -0
- package/dist-public/src/discoveryGmail.d.ts +12 -0
- package/dist-public/src/discoveryGmail.js +466 -0
- package/dist-public/src/discoveryGong.d.ts +12 -0
- package/dist-public/src/discoveryGong.js +491 -0
- package/dist-public/src/discoveryLearningRouting.d.ts +90 -0
- package/dist-public/src/discoveryLearningRouting.js +405 -0
- package/dist-public/src/discoveryMatching.d.ts +26 -0
- package/dist-public/src/discoveryMatching.js +73 -0
- package/dist-public/src/discoveryOutlook.d.ts +12 -0
- package/dist-public/src/discoveryOutlook.js +492 -0
- package/dist-public/src/discoveryProcess.d.ts +221 -0
- package/dist-public/src/discoveryProcess.js +703 -0
- package/dist-public/src/discoveryProof.d.ts +143 -0
- package/dist-public/src/discoveryProof.js +529 -0
- package/dist-public/src/discoveryProviders.d.ts +152 -0
- package/dist-public/src/discoveryProviders.js +756 -0
- package/dist-public/src/discoveryReadiness.d.ts +219 -0
- package/dist-public/src/discoveryReadiness.js +672 -0
- package/dist-public/src/discoveryReconcile.d.ts +57 -0
- package/dist-public/src/discoveryReconcile.js +424 -0
- package/dist-public/src/discoverySlack.d.ts +12 -0
- package/dist-public/src/discoverySlack.js +452 -0
- package/dist-public/src/discoverySources.d.ts +56 -0
- package/dist-public/src/discoverySources.js +322 -0
- package/dist-public/src/discoverySync.d.ts +52 -0
- package/dist-public/src/discoverySync.js +151 -0
- package/dist-public/src/discoverySynthesis.d.ts +57 -0
- package/dist-public/src/discoverySynthesis.js +215 -0
- package/dist-public/src/discoveryTeams.d.ts +12 -0
- package/dist-public/src/discoveryTeams.js +516 -0
- package/dist-public/src/discoveryZoom.d.ts +12 -0
- package/dist-public/src/discoveryZoom.js +523 -0
- package/dist-public/src/elicitationSurface.d.ts +13 -0
- package/dist-public/src/elicitationSurface.js +77 -0
- package/dist-public/src/errorReport.d.ts +99 -0
- package/dist-public/src/errorReport.js +443 -0
- package/dist-public/src/errorTaxonomy.d.ts +84 -0
- package/dist-public/src/errorTaxonomy.js +189 -0
- package/dist-public/src/evalCalibration.d.ts +82 -0
- package/dist-public/src/evalCalibration.js +169 -0
- package/dist-public/src/evalChecks.d.ts +21 -0
- package/dist-public/src/evalChecks.js +295 -0
- package/dist-public/src/evalCompile.d.ts +56 -0
- package/dist-public/src/evalCompile.js +58 -0
- package/dist-public/src/evalImprove.d.ts +60 -0
- package/dist-public/src/evalImprove.js +175 -0
- package/dist-public/src/evalJudge.d.ts +47 -0
- package/dist-public/src/evalJudge.js +143 -0
- package/dist-public/src/evalLabels.d.ts +100 -0
- package/dist-public/src/evalLabels.js +287 -0
- package/dist-public/src/evalRecord.d.ts +68 -0
- package/dist-public/src/evalRecord.js +165 -0
- package/dist-public/src/evalRoast.d.ts +88 -0
- package/dist-public/src/evalRoast.js +249 -0
- package/dist-public/src/evidenceIntake.d.ts +66 -0
- package/dist-public/src/evidenceIntake.js +356 -0
- package/dist-public/src/feedback.d.ts +78 -0
- package/dist-public/src/feedback.js +294 -0
- package/dist-public/src/firefliesClient.d.ts +106 -0
- package/dist-public/src/firefliesClient.js +258 -0
- package/dist-public/src/firstMoveTensions.d.ts +10 -0
- package/dist-public/src/firstMoveTensions.js +93 -0
- package/dist-public/src/gitRun.d.ts +21 -0
- package/dist-public/src/gitRun.js +50 -0
- package/dist-public/src/githubReconcile.d.ts +94 -0
- package/dist-public/src/githubReconcile.js +205 -0
- package/dist-public/src/gmailNango.d.ts +131 -0
- package/dist-public/src/gmailNango.js +329 -0
- package/dist-public/src/gongNango.d.ts +118 -0
- package/dist-public/src/gongNango.js +308 -0
- package/dist-public/src/grammarGate.d.ts +13 -0
- package/dist-public/src/grammarGate.js +72 -0
- package/dist-public/src/harnessGates.d.ts +65 -0
- package/dist-public/src/harnessGates.js +168 -0
- package/dist-public/src/humanSurfaceEval.d.ts +19 -0
- package/dist-public/src/humanSurfaceEval.js +156 -0
- package/dist-public/src/ideate.d.ts +43 -0
- package/dist-public/src/ideate.js +255 -0
- package/dist-public/src/ideateContext.d.ts +11 -0
- package/dist-public/src/ideateContext.js +83 -0
- package/dist-public/src/index.d.ts +2 -0
- package/dist-public/src/index.js +4 -0
- package/dist-public/src/initCommand.d.ts +25 -0
- package/dist-public/src/initCommand.js +558 -0
- package/dist-public/src/interviewGate.d.ts +25 -0
- package/dist-public/src/interviewGate.js +126 -0
- package/dist-public/src/jsonFile.d.ts +2 -0
- package/dist-public/src/jsonFile.js +22 -0
- package/dist-public/src/loopBudget.d.ts +182 -0
- package/dist-public/src/loopBudget.js +494 -0
- package/dist-public/src/loopContracts.d.ts +35 -0
- package/dist-public/src/loopContracts.js +364 -0
- package/dist-public/src/machineState.d.ts +1 -0
- package/dist-public/src/machineState.js +9 -0
- package/dist-public/src/notionNango.d.ts +93 -0
- package/dist-public/src/notionNango.js +279 -0
- package/dist-public/src/orchestrationResult.d.ts +11 -0
- package/dist-public/src/orchestrationResult.js +73 -0
- package/dist-public/src/outlookNango.d.ts +124 -0
- package/dist-public/src/outlookNango.js +327 -0
- package/dist-public/src/packetCorrections.d.ts +40 -0
- package/dist-public/src/packetCorrections.js +172 -0
- package/dist-public/src/packetGlossary.d.ts +6 -0
- package/dist-public/src/packetGlossary.js +42 -0
- package/dist-public/src/packetIndex.d.ts +54 -0
- package/dist-public/src/packetIndex.js +744 -0
- package/dist-public/src/packetIndexEntry.d.ts +3 -0
- package/dist-public/src/packetIndexEntry.js +128 -0
- package/dist-public/src/packetIndexServe.d.ts +13 -0
- package/dist-public/src/packetIndexServe.js +113 -0
- package/dist-public/src/packetIndexWorker.d.ts +7 -0
- package/dist-public/src/packetIndexWorker.js +45 -0
- package/dist-public/src/packetLifecycle.d.ts +30 -0
- package/dist-public/src/packetLifecycle.js +127 -0
- package/dist-public/src/packetReconcile.d.ts +73 -0
- package/dist-public/src/packetReconcile.js +386 -0
- package/dist-public/src/packetStatus.d.ts +125 -0
- package/dist-public/src/packetStatus.js +770 -0
- package/dist-public/src/packetValidation.d.ts +17 -0
- package/dist-public/src/packetValidation.js +333 -0
- package/dist-public/src/packetVerify.d.ts +73 -0
- package/dist-public/src/packetVerify.js +303 -0
- package/dist-public/src/packetWrite.d.ts +52 -0
- package/dist-public/src/packetWrite.js +448 -0
- package/dist-public/src/pitch.d.ts +38 -0
- package/dist-public/src/pitch.js +154 -0
- package/dist-public/src/processMemo.d.ts +8 -0
- package/dist-public/src/processMemo.js +23 -0
- package/dist-public/src/productDetect.d.ts +30 -0
- package/dist-public/src/productDetect.js +376 -0
- package/dist-public/src/profile.d.ts +62 -0
- package/dist-public/src/profile.js +334 -0
- package/dist-public/src/questionModel.d.ts +46 -0
- package/dist-public/src/questionModel.js +74 -0
- package/dist-public/src/questionText.d.ts +7 -0
- package/dist-public/src/questionText.js +38 -0
- package/dist-public/src/redactConfig.d.ts +2 -0
- package/dist-public/src/redactConfig.js +11 -0
- package/dist-public/src/redactEngine.d.ts +59 -0
- package/dist-public/src/redactEngine.js +509 -0
- package/dist-public/src/redactGate.d.ts +63 -0
- package/dist-public/src/redactGate.js +209 -0
- package/dist-public/src/repoRead.d.ts +29 -0
- package/dist-public/src/repoRead.js +160 -0
- package/dist-public/src/research.d.ts +242 -0
- package/dist-public/src/research.js +1314 -0
- package/dist-public/src/researchCompletion.d.ts +34 -0
- package/dist-public/src/researchCompletion.js +123 -0
- package/dist-public/src/researchExecution.d.ts +47 -0
- package/dist-public/src/researchExecution.js +309 -0
- package/dist-public/src/researchFormat.d.ts +21 -0
- package/dist-public/src/researchFormat.js +77 -0
- package/dist-public/src/researchGate.d.ts +86 -0
- package/dist-public/src/researchGate.js +1036 -0
- package/dist-public/src/researchReceipt.d.ts +76 -0
- package/dist-public/src/researchReceipt.js +148 -0
- package/dist-public/src/researchRender.d.ts +2 -0
- package/dist-public/src/researchRender.js +267 -0
- package/dist-public/src/researchSigningKey.d.ts +19 -0
- package/dist-public/src/researchSigningKey.js +95 -0
- package/dist-public/src/riskClassify.d.ts +42 -0
- package/dist-public/src/riskClassify.js +149 -0
- package/dist-public/src/routines.d.ts +42 -0
- package/dist-public/src/routines.js +269 -0
- package/dist-public/src/routingPolicy.d.ts +172 -0
- package/dist-public/src/routingPolicy.js +646 -0
- package/dist-public/src/secretInput.d.ts +8 -0
- package/dist-public/src/secretInput.js +89 -0
- package/dist-public/src/selfCheck.d.ts +7 -0
- package/dist-public/src/selfCheck.js +473 -0
- package/dist-public/src/selfCheckCache.d.ts +29 -0
- package/dist-public/src/selfCheckCache.js +147 -0
- package/dist-public/src/severityClass.d.ts +15 -0
- package/dist-public/src/severityClass.js +22 -0
- package/dist-public/src/skillClientPolicy.d.ts +8 -0
- package/dist-public/src/skillClientPolicy.js +16 -0
- package/dist-public/src/skillLocal.d.ts +98 -0
- package/dist-public/src/skillLocal.js +463 -0
- package/dist-public/src/skillTreeDigest.d.ts +9 -0
- package/dist-public/src/skillTreeDigest.js +64 -0
- package/dist-public/src/slackNango.d.ts +62 -0
- package/dist-public/src/slackNango.js +208 -0
- package/dist-public/src/social/arxiv.d.ts +19 -0
- package/dist-public/src/social/arxiv.js +96 -0
- package/dist-public/src/social/github.d.ts +2 -0
- package/dist-public/src/social/github.js +78 -0
- package/dist-public/src/social/hackernews.d.ts +2 -0
- package/dist-public/src/social/hackernews.js +67 -0
- package/dist-public/src/social/index.d.ts +10 -0
- package/dist-public/src/social/index.js +79 -0
- package/dist-public/src/social/reddit.d.ts +2 -0
- package/dist-public/src/social/reddit.js +125 -0
- package/dist-public/src/social/types.d.ts +73 -0
- package/dist-public/src/social/types.js +61 -0
- package/dist-public/src/social/xml.d.ts +10 -0
- package/dist-public/src/social/xml.js +49 -0
- package/dist-public/src/social/youtube.d.ts +7 -0
- package/dist-public/src/social/youtube.js +217 -0
- package/dist-public/src/sourceIngress.d.ts +488 -0
- package/dist-public/src/sourceIngress.js +1651 -0
- package/dist-public/src/specAfter.d.ts +187 -0
- package/dist-public/src/specAfter.js +814 -0
- package/dist-public/src/specBudget.d.ts +124 -0
- package/dist-public/src/specBudget.js +424 -0
- package/dist-public/src/specDecisionSignals.d.ts +38 -0
- package/dist-public/src/specDecisionSignals.js +121 -0
- package/dist-public/src/specLinkedCommits.d.ts +7 -0
- package/dist-public/src/specLinkedCommits.js +107 -0
- package/dist-public/src/specSections.d.ts +105 -0
- package/dist-public/src/specSections.js +718 -0
- package/dist-public/src/statusGlyphs.d.ts +18 -0
- package/dist-public/src/statusGlyphs.js +75 -0
- package/dist-public/src/swarmCommand.d.ts +10 -0
- package/dist-public/src/swarmCommand.js +96 -0
- package/dist-public/src/teamsNango.d.ts +114 -0
- package/dist-public/src/teamsNango.js +301 -0
- package/dist-public/src/tokenStore.d.ts +10 -0
- package/dist-public/src/tokenStore.js +77 -0
- package/dist-public/src/trustEnvelope.d.ts +78 -0
- package/dist-public/src/trustEnvelope.js +392 -0
- package/dist-public/src/untrustedText.d.ts +6 -0
- package/dist-public/src/untrustedText.js +20 -0
- package/dist-public/src/updateNotice.d.ts +41 -0
- package/dist-public/src/updateNotice.js +290 -0
- package/dist-public/src/vendorYaml.d.ts +1 -0
- package/dist-public/src/vendorYaml.js +7374 -0
- package/dist-public/src/workflowRunActuals.d.ts +67 -0
- package/dist-public/src/workflowRunActuals.js +184 -0
- package/dist-public/src/zendeskDiscovery.d.ts +12 -0
- package/dist-public/src/zendeskDiscovery.js +513 -0
- package/dist-public/src/zendeskNango.d.ts +131 -0
- package/dist-public/src/zendeskNango.js +344 -0
- package/dist-public/src/zoomNango.d.ts +106 -0
- package/dist-public/src/zoomNango.js +351 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/THIRD_PARTY_NOTICES.md +36 -0
- package/package.json +71 -4
- package/schemas/bug-capsule-receipt.schema.json +54 -0
- package/schemas/bug-capsule.schema.json +189 -0
- package/schemas/discovery-evidence-envelope.schema.json +79 -0
- package/schemas/discovery-processing-receipt.schema.json +71 -0
- package/schemas/discovery-service-contract.schema.json +189 -0
- package/skills/AGENTS.md +32 -0
- package/skills/README.md +287 -0
- package/skills/bug/SKILL.md +49 -0
- package/skills/build-preview-spec/SKILL.md +103 -0
- package/skills/capture-external-artifact/SKILL.md +23 -0
- package/skills/capture-external-artifact/agents/openai.yaml +4 -0
- package/skills/capture-external-artifact/references/extractor-libraries.md +27 -0
- package/skills/check-spec-health/SKILL.md +45 -0
- package/skills/check-spec-health/agents/openai.yaml +4 -0
- package/skills/competitive-positioning/SKILL.md +86 -0
- package/skills/competitive-positioning/agents/openai.yaml +4 -0
- package/skills/cpo-lens/SKILL.md +398 -0
- package/skills/cpo-lens/agents/openai.yaml +4 -0
- package/skills/create-prd/SKILL.md +26 -0
- package/skills/create-prd/agents/openai.yaml +4 -0
- package/skills/customer-discovery/SKILL.md +192 -0
- package/skills/design-system/SKILL.md +49 -0
- package/skills/design-system/agents/openai.yaml +4 -0
- package/skills/discussion-guide/SKILL.md +75 -0
- package/skills/discussion-guide/agents/openai.yaml +4 -0
- package/skills/firecrawl-research-papers/SKILL.md +96 -0
- package/skills/firecrawl-research-papers/agents/openai.yaml +4 -0
- package/skills/foo-roast/SKILL.md +325 -0
- package/skills/generate-gtm-brief/SKILL.md +41 -0
- package/skills/generate-gtm-brief/agents/openai.yaml +4 -0
- package/skills/generate-gtm-brief/references/gtm-brief-template.md +56 -0
- package/skills/gong-quotes/SKILL.md +43 -0
- package/skills/gong-quotes/agents/openai.yaml +4 -0
- package/skills/grill-me/SKILL.md +27 -0
- package/skills/grill-me/agents/openai.yaml +4 -0
- package/skills/ideate/SKILL.md +180 -0
- package/skills/ideate/references/family-packs.md +5 -0
- package/skills/initiative-tracker/SKILL.md +23 -0
- package/skills/initiative-tracker/agents/openai.yaml +4 -0
- package/skills/interop-review/SKILL.md +25 -0
- package/skills/interop-review/agents/openai.yaml +4 -0
- package/skills/interview-me/SKILL.md +404 -0
- package/skills/interview-me/agents/openai.yaml +4 -0
- package/skills/loop-library-audit/SKILL.md +123 -0
- package/skills/pitch/SKILL.md +110 -0
- package/skills/pitch/references/opportunity-assessment.md +136 -0
- package/skills/pitch/references/prfaq-template.md +54 -0
- package/skills/pmm-release-briefing/SKILL.md +27 -0
- package/skills/pmm-release-briefing/agents/openai.yaml +4 -0
- package/skills/pmm-release-briefing/references/variant-generalized.md +16 -0
- package/skills/positioning-messaging/SKILL.md +70 -0
- package/skills/positioning-messaging/agents/openai.yaml +4 -0
- package/skills/positioning-messaging/references/guest-insights.md +1400 -0
- package/skills/preview-swarm/SKILL.md +418 -0
- package/skills/preview-swarm/agents/openai.yaml +4 -0
- package/skills/preview-swarm/references/html-effectiveness-build-previews.md +168 -0
- package/skills/product-copywriting/SKILL.md +127 -0
- package/skills/product-copywriting/agents/openai.yaml +4 -0
- package/skills/product-copywriting/references/foo-voice.md +26 -0
- package/skills/product-shape-interview/SKILL.md +219 -0
- package/skills/product-shape-interview/agents/openai.yaml +4 -0
- package/skills/ready-for-release/SKILL.md +39 -0
- package/skills/ready-for-release/agents/openai.yaml +4 -0
- package/skills/redact-transcripts/SKILL.md +23 -0
- package/skills/redact-transcripts/agents/openai.yaml +4 -0
- package/skills/research-brief/SKILL.md +48 -0
- package/skills/research-brief/agents/openai.yaml +4 -0
- package/skills/research-refresh/SKILL.md +63 -0
- package/skills/research-refresh/references/worked-cases.md +44 -0
- package/skills/review-loop/SKILL.md +200 -0
- package/skills/review-spec/SKILL.md +32 -0
- package/skills/review-spec/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/SKILL.md +42 -0
- package/skills/rfc-from-research/agents/openai.yaml +4 -0
- package/skills/rfc-from-research/references/rfc-pattern.md +69 -0
- package/skills/skill-registry.yaml +810 -0
- package/skills/spec-cleanup/SKILL.md +103 -0
- package/skills/spec-cleanup/agents/openai.yaml +4 -0
- package/skills/spec-swarm/SKILL.md +87 -0
- package/skills/spec-swarm/agents/openai.yaml +4 -0
- package/skills/spec-swarm/template/SKILL.template.md +94 -0
- package/skills/spec-swarm/template/personas/accessibility-inclusion.md +40 -0
- package/skills/spec-swarm/template/personas/ai-technical-pm.md +69 -0
- package/skills/spec-swarm/template/personas/analytics-engineering.md +39 -0
- package/skills/spec-swarm/template/personas/applied-ai-eval.md +64 -0
- package/skills/spec-swarm/template/personas/business-gm-pm.md +67 -0
- package/skills/spec-swarm/template/personas/consumer-craft-pm.md +68 -0
- package/skills/spec-swarm/template/personas/content-design.md +40 -0
- package/skills/spec-swarm/template/personas/cpo.md +42 -0
- package/skills/spec-swarm/template/personas/data-science.md +42 -0
- package/skills/spec-swarm/template/personas/developer-experience.md +65 -0
- package/skills/spec-swarm/template/personas/dx-product.md +70 -0
- package/skills/spec-swarm/template/personas/field.md +43 -0
- package/skills/spec-swarm/template/personas/finance-cfo.md +40 -0
- package/skills/spec-swarm/template/personas/growth-pm.md +67 -0
- package/skills/spec-swarm/template/personas/platform-pm.md +67 -0
- package/skills/spec-swarm/template/personas/pmm.md +43 -0
- package/skills/spec-swarm/template/personas/principal-product-designer.md +42 -0
- package/skills/spec-swarm/template/personas/product-ops-program.md +41 -0
- package/skills/spec-swarm/template/personas/qa-release.md +41 -0
- package/skills/spec-swarm/template/personas/responsible-ai-safety.md +64 -0
- package/skills/spec-swarm/template/personas/security-privacy-legal.md +40 -0
- package/skills/spec-swarm/template/personas/strategy-competitive.md +41 -0
- package/skills/spec-swarm/template/personas/technical-architect.md +42 -0
- package/skills/spec-swarm/template/personas/technical-lead.md +43 -0
- package/skills/spec-swarm/template/personas/user-research.md +41 -0
- package/skills/spec-swarm-accessibility-inclusion/SKILL.md +95 -0
- package/skills/spec-swarm-accessibility-inclusion/agents/openai.yaml +4 -0
- package/skills/spec-swarm-ai-technical-pm/SKILL.md +124 -0
- package/skills/spec-swarm-analytics-engineering/SKILL.md +94 -0
- package/skills/spec-swarm-analytics-engineering/agents/openai.yaml +4 -0
- package/skills/spec-swarm-applied-ai-eval/SKILL.md +120 -0
- package/skills/spec-swarm-business-gm-pm/SKILL.md +122 -0
- package/skills/spec-swarm-consumer-craft-pm/SKILL.md +122 -0
- package/skills/spec-swarm-content-design/SKILL.md +95 -0
- package/skills/spec-swarm-content-design/agents/openai.yaml +4 -0
- package/skills/spec-swarm-cpo/SKILL.md +94 -0
- package/skills/spec-swarm-cpo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-data-science/SKILL.md +95 -0
- package/skills/spec-swarm-data-science/agents/openai.yaml +4 -0
- package/skills/spec-swarm-developer-experience/SKILL.md +121 -0
- package/skills/spec-swarm-dx-product/SKILL.md +126 -0
- package/skills/spec-swarm-field/SKILL.md +96 -0
- package/skills/spec-swarm-field/agents/openai.yaml +4 -0
- package/skills/spec-swarm-finance-cfo/SKILL.md +95 -0
- package/skills/spec-swarm-finance-cfo/agents/openai.yaml +4 -0
- package/skills/spec-swarm-growth-pm/SKILL.md +122 -0
- package/skills/spec-swarm-platform-pm/SKILL.md +122 -0
- package/skills/spec-swarm-pmm/SKILL.md +96 -0
- package/skills/spec-swarm-pmm/agents/openai.yaml +4 -0
- package/skills/spec-swarm-principal-product-designer/SKILL.md +95 -0
- package/skills/spec-swarm-principal-product-designer/agents/openai.yaml +4 -0
- package/skills/spec-swarm-product-ops-program/SKILL.md +96 -0
- package/skills/spec-swarm-product-ops-program/agents/openai.yaml +4 -0
- package/skills/spec-swarm-qa-release/SKILL.md +96 -0
- package/skills/spec-swarm-qa-release/agents/openai.yaml +4 -0
- package/skills/spec-swarm-responsible-ai-safety/SKILL.md +120 -0
- package/skills/spec-swarm-security-privacy-legal/SKILL.md +95 -0
- package/skills/spec-swarm-security-privacy-legal/agents/openai.yaml +4 -0
- package/skills/spec-swarm-strategy-competitive/SKILL.md +96 -0
- package/skills/spec-swarm-strategy-competitive/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-architect/SKILL.md +95 -0
- package/skills/spec-swarm-technical-architect/agents/openai.yaml +4 -0
- package/skills/spec-swarm-technical-lead/SKILL.md +96 -0
- package/skills/spec-swarm-technical-lead/agents/openai.yaml +4 -0
- package/skills/spec-swarm-user-research/SKILL.md +95 -0
- package/skills/spec-swarm-user-research/agents/openai.yaml +4 -0
- package/skills/strategy-check/SKILL.md +170 -0
- package/skills/strategy-check/agents/openai.yaml +4 -0
- package/skills/study-digest/SKILL.md +41 -0
- package/skills/synthetic-customer/SKILL.md +68 -0
- package/skills/update-cli-spec/SKILL.md +69 -0
- package/skills/update-cli-spec/agents/openai.yaml +4 -0
- package/skills/voc-researcher/SKILL.md +45 -0
- package/skills/voc-researcher/agents/openai.yaml +4 -0
- package/skills-lock.json +35 -0
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { companyFromEmailDomain, contactHintsFromDisplayLabel, dedupeContactHints, isValidLinkedinUrl, sanitizeDiscoveryStableUrl, } from "./discoverySources.js";
|
|
2
|
+
const GRANOLA_MEETING_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3
|
+
function granolaStableUrl(raw) {
|
|
4
|
+
if (raw.url)
|
|
5
|
+
return sanitizeDiscoveryStableUrl("granola", raw.url);
|
|
6
|
+
if (!GRANOLA_MEETING_UUID.test(raw.id))
|
|
7
|
+
return undefined;
|
|
8
|
+
return sanitizeDiscoveryStableUrl("granola", `https://notes.granola.ai/d/${raw.id}`);
|
|
9
|
+
}
|
|
10
|
+
// Contact capture at ingest (discovery-contact-capture-v0 R-003/R-004): the
|
|
11
|
+
// provider's own attendee metadata seeds contact-card candidates instead of
|
|
12
|
+
// being discarded. Display strings yield name (+company); the email DOMAIN
|
|
13
|
+
// yields the company and the raw address is discarded right here — it never
|
|
14
|
+
// enters the normalized session; title and LinkedIn ride only when the payload
|
|
15
|
+
// actually carries them.
|
|
16
|
+
function granolaContactHints(raw) {
|
|
17
|
+
const hints = [];
|
|
18
|
+
for (const attendee of raw.attendees ?? []) {
|
|
19
|
+
const name = attendee.name?.trim();
|
|
20
|
+
if (name)
|
|
21
|
+
hints.push(...contactHintsFromDisplayLabel(name, "granola:attendee"));
|
|
22
|
+
const email = attendee.email?.trim();
|
|
23
|
+
if (email) {
|
|
24
|
+
const company = companyFromEmailDomain(email);
|
|
25
|
+
if (company)
|
|
26
|
+
hints.push({ field: "company", value: company, source: "granola:attendee_email_domain" });
|
|
27
|
+
}
|
|
28
|
+
const company = attendee.company?.trim();
|
|
29
|
+
if (company)
|
|
30
|
+
hints.push({ field: "company", value: company, source: "granola:attendee" });
|
|
31
|
+
const title = attendee.title?.trim();
|
|
32
|
+
if (title)
|
|
33
|
+
hints.push({ field: "title", value: title, source: "granola:attendee" });
|
|
34
|
+
const linkedin = attendee.linkedin?.trim();
|
|
35
|
+
if (linkedin && isValidLinkedinUrl(linkedin))
|
|
36
|
+
hints.push({ field: "linkedin", value: linkedin, source: "granola:attendee" });
|
|
37
|
+
}
|
|
38
|
+
const deduped = dedupeContactHints(hints);
|
|
39
|
+
return deduped.length ? deduped : undefined;
|
|
40
|
+
}
|
|
41
|
+
export const granolaDiscoveryAdapter = {
|
|
42
|
+
provider: "granola",
|
|
43
|
+
normalizeSession(raw, connectionAlias) {
|
|
44
|
+
const contactHints = granolaContactHints(raw);
|
|
45
|
+
const stableUrl = granolaStableUrl(raw);
|
|
46
|
+
return {
|
|
47
|
+
schema_version: "0.1",
|
|
48
|
+
provider: "granola",
|
|
49
|
+
connection_alias: connectionAlias,
|
|
50
|
+
runtime_handle: raw.id,
|
|
51
|
+
completed_at: new Date(raw.datetime).toISOString(),
|
|
52
|
+
completion_state: raw.status === "completed" ? "completed" : "in_progress",
|
|
53
|
+
title: raw.title.trim(),
|
|
54
|
+
participants: (raw.attendees ?? []).map((attendee) => attendee.name?.trim() ?? "").filter(Boolean),
|
|
55
|
+
stable_url: stableUrl,
|
|
56
|
+
// open_transcript is observed, not assumed (R-004): a note without an
|
|
57
|
+
// observed transcript grant normalizes with open_transcript: false, and
|
|
58
|
+
// the readiness operation is where the needs_provider_access stop lives.
|
|
59
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_access === "granted", stable_link: Boolean(stableUrl), timestamps: true },
|
|
60
|
+
access: "unknown",
|
|
61
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const GONG_CALL_ID = /^[A-Za-z0-9_-]{4,64}$/;
|
|
66
|
+
function gongStableUrl(raw) {
|
|
67
|
+
if (raw.url)
|
|
68
|
+
return sanitizeDiscoveryStableUrl("gong", raw.url);
|
|
69
|
+
if (!GONG_CALL_ID.test(raw.id))
|
|
70
|
+
return undefined;
|
|
71
|
+
return sanitizeDiscoveryStableUrl("gong", `https://app.gong.io/call?id=${raw.id}`);
|
|
72
|
+
}
|
|
73
|
+
function gongCompletedAt(raw) {
|
|
74
|
+
if (raw.ended)
|
|
75
|
+
return new Date(raw.ended).toISOString();
|
|
76
|
+
const started = new Date(raw.started).getTime();
|
|
77
|
+
const duration = Number.isFinite(raw.duration_seconds) ? Number(raw.duration_seconds) * 1000 : 0;
|
|
78
|
+
return new Date(started + duration).toISOString();
|
|
79
|
+
}
|
|
80
|
+
// Contact hints follow the Granola discipline (discovery-contact-capture-v0
|
|
81
|
+
// R-003/R-004) with one Gong-specific rule: a party the provider marks as
|
|
82
|
+
// internal is the company's own rep, not a customer contact candidate, so it
|
|
83
|
+
// contributes no hint. Everyone still appears in `participants` for matching.
|
|
84
|
+
function gongContactHints(raw) {
|
|
85
|
+
const hints = [];
|
|
86
|
+
for (const party of raw.parties ?? []) {
|
|
87
|
+
if (party.affiliation?.trim().toLowerCase() === "internal")
|
|
88
|
+
continue;
|
|
89
|
+
const name = party.name?.trim();
|
|
90
|
+
if (name)
|
|
91
|
+
hints.push(...contactHintsFromDisplayLabel(name, "gong:party"));
|
|
92
|
+
const email = party.email?.trim();
|
|
93
|
+
if (email) {
|
|
94
|
+
const company = companyFromEmailDomain(email);
|
|
95
|
+
if (company)
|
|
96
|
+
hints.push({ field: "company", value: company, source: "gong:party_email_domain" });
|
|
97
|
+
}
|
|
98
|
+
const title = party.title?.trim();
|
|
99
|
+
if (title)
|
|
100
|
+
hints.push({ field: "title", value: title, source: "gong:party" });
|
|
101
|
+
}
|
|
102
|
+
const deduped = dedupeContactHints(hints);
|
|
103
|
+
return deduped.length ? deduped : undefined;
|
|
104
|
+
}
|
|
105
|
+
// Participant labels are party display names plus, for each external party
|
|
106
|
+
// with an address, a domain-only label ("@exampletonbros.example"). The domain label
|
|
107
|
+
// is what an inbox scope's domain_allowlist selects on; the local part of the
|
|
108
|
+
// address is discarded here and never leaves the adapter (R-004/R-005).
|
|
109
|
+
function gongParticipantLabels(raw) {
|
|
110
|
+
const labels = [];
|
|
111
|
+
for (const party of raw.parties ?? []) {
|
|
112
|
+
const name = party.name?.trim();
|
|
113
|
+
if (name)
|
|
114
|
+
labels.push(name);
|
|
115
|
+
if (party.affiliation?.trim().toLowerCase() === "internal")
|
|
116
|
+
continue;
|
|
117
|
+
const email = party.email?.trim().toLowerCase();
|
|
118
|
+
const at = email ? email.lastIndexOf("@") : -1;
|
|
119
|
+
if (email && at > 0 && at < email.length - 1)
|
|
120
|
+
labels.push(`@${email.slice(at + 1)}`);
|
|
121
|
+
}
|
|
122
|
+
return [...new Set(labels)];
|
|
123
|
+
}
|
|
124
|
+
export const gongDiscoveryAdapter = {
|
|
125
|
+
provider: "gong",
|
|
126
|
+
normalizeSession(raw, connectionAlias) {
|
|
127
|
+
const contactHints = gongContactHints(raw);
|
|
128
|
+
const stableUrl = gongStableUrl(raw);
|
|
129
|
+
return {
|
|
130
|
+
schema_version: "0.1",
|
|
131
|
+
provider: "gong",
|
|
132
|
+
connection_alias: connectionAlias,
|
|
133
|
+
runtime_handle: raw.id,
|
|
134
|
+
completed_at: gongCompletedAt(raw),
|
|
135
|
+
completion_state: raw.state === "completed" ? "completed" : "in_progress",
|
|
136
|
+
title: raw.title.trim(),
|
|
137
|
+
participants: gongParticipantLabels(raw),
|
|
138
|
+
stable_url: stableUrl,
|
|
139
|
+
// timestamps: Gong transcript sentences carry millisecond offsets, so
|
|
140
|
+
// envelope locators can cite them. open_transcript is observed, not
|
|
141
|
+
// assumed (R-005).
|
|
142
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_access === "granted", stable_link: Boolean(stableUrl), timestamps: true },
|
|
143
|
+
access: "unknown",
|
|
144
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const FIREFLIES_TRANSCRIPT_ID = /^[A-Za-z0-9_-]{8,64}$/;
|
|
149
|
+
function firefliesStableUrl(raw) {
|
|
150
|
+
if (raw.url)
|
|
151
|
+
return sanitizeDiscoveryStableUrl("fireflies", raw.url);
|
|
152
|
+
if (!FIREFLIES_TRANSCRIPT_ID.test(raw.id))
|
|
153
|
+
return undefined;
|
|
154
|
+
// Belief, owned by the packet's feasibility task (T-001): Fireflies links a
|
|
155
|
+
// transcript as a path under app.fireflies.ai.
|
|
156
|
+
return sanitizeDiscoveryStableUrl("fireflies", `https://app.fireflies.ai/transcript/${raw.id}`);
|
|
157
|
+
}
|
|
158
|
+
// The distinct email domains the transcript's own participant metadata
|
|
159
|
+
// carries. OQ-002: Fireflies' role attribution (internal/external) is
|
|
160
|
+
// unverified, so externality derives from email domains only.
|
|
161
|
+
function firefliesParticipantDomains(raw) {
|
|
162
|
+
const domains = new Set();
|
|
163
|
+
for (const participant of raw.participants ?? []) {
|
|
164
|
+
const email = participant.email?.trim().toLowerCase();
|
|
165
|
+
const at = email ? email.lastIndexOf("@") : -1;
|
|
166
|
+
if (email && at > 0 && at < email.length - 1)
|
|
167
|
+
domains.add(email.slice(at + 1));
|
|
168
|
+
}
|
|
169
|
+
return [...domains];
|
|
170
|
+
}
|
|
171
|
+
// A transcript with every identified participant on ONE shared domain shows no
|
|
172
|
+
// external party by domain — the only internal signal the email-domain rule
|
|
173
|
+
// (OQ-002) can support — and is excluded by default. A transcript whose
|
|
174
|
+
// participants carry no emails, or whose emails span domains, is not excluded
|
|
175
|
+
// here; the scope's selectors still have to admit it. Domain-only labels and
|
|
176
|
+
// the internal-only rule are re-derived by the worker from this helper.
|
|
177
|
+
export function firefliesInternalOnly(raw) {
|
|
178
|
+
const domains = firefliesParticipantDomains(raw);
|
|
179
|
+
return domains.length === 1;
|
|
180
|
+
}
|
|
181
|
+
// Contact hints follow the Granola discipline (discovery-contact-capture-v0
|
|
182
|
+
// R-003/R-004). Without provider-declared affiliation (OQ-002) a mixed-domain
|
|
183
|
+
// transcript cannot attribute internality per participant, so every emailed
|
|
184
|
+
// participant contributes a DOMAIN-ONLY company hint and every well-formed
|
|
185
|
+
// display name a full_name hint — candidates for a person to confirm, never
|
|
186
|
+
// confirmed identity. An internal-only transcript (everyone on one domain)
|
|
187
|
+
// yields no hints: its only domain is the company's own.
|
|
188
|
+
function firefliesContactHints(raw) {
|
|
189
|
+
if (firefliesInternalOnly(raw))
|
|
190
|
+
return undefined;
|
|
191
|
+
const hints = [];
|
|
192
|
+
for (const participant of raw.participants ?? []) {
|
|
193
|
+
const name = participant.display_name?.trim();
|
|
194
|
+
if (name)
|
|
195
|
+
hints.push(...contactHintsFromDisplayLabel(name, "fireflies:participant"));
|
|
196
|
+
const email = participant.email?.trim();
|
|
197
|
+
if (email) {
|
|
198
|
+
const company = companyFromEmailDomain(email);
|
|
199
|
+
if (company)
|
|
200
|
+
hints.push({ field: "company", value: company, source: "fireflies:participant_email_domain" });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const deduped = dedupeContactHints(hints);
|
|
204
|
+
return deduped.length ? deduped : undefined;
|
|
205
|
+
}
|
|
206
|
+
// Participant labels are speaker display names plus, for each participant with
|
|
207
|
+
// an address, a domain-only label ("@exampletonbros.example") — the label an inbox
|
|
208
|
+
// scope's domain_allowlist selects on. OQ-002: with no affiliation field, no
|
|
209
|
+
// label is filtered; the raw email dies here and never leaves the adapter.
|
|
210
|
+
function firefliesParticipantLabels(raw) {
|
|
211
|
+
const labels = [];
|
|
212
|
+
for (const participant of raw.participants ?? []) {
|
|
213
|
+
const name = participant.display_name?.trim();
|
|
214
|
+
if (name)
|
|
215
|
+
labels.push(name);
|
|
216
|
+
const email = participant.email?.trim().toLowerCase();
|
|
217
|
+
const at = email ? email.lastIndexOf("@") : -1;
|
|
218
|
+
if (email && at > 0 && at < email.length - 1)
|
|
219
|
+
labels.push(`@${email.slice(at + 1)}`);
|
|
220
|
+
}
|
|
221
|
+
return [...new Set(labels)];
|
|
222
|
+
}
|
|
223
|
+
export const firefliesDiscoveryAdapter = {
|
|
224
|
+
provider: "fireflies",
|
|
225
|
+
normalizeSession(raw, connectionAlias) {
|
|
226
|
+
const contactHints = firefliesContactHints(raw);
|
|
227
|
+
const stableUrl = firefliesStableUrl(raw);
|
|
228
|
+
return {
|
|
229
|
+
schema_version: "0.1",
|
|
230
|
+
provider: "fireflies",
|
|
231
|
+
connection_alias: connectionAlias,
|
|
232
|
+
runtime_handle: raw.id,
|
|
233
|
+
// The meeting's end time; the start is the honest fallback when the
|
|
234
|
+
// provider reports no end (R-005).
|
|
235
|
+
completed_at: new Date(raw.ended ?? raw.started_at).toISOString(),
|
|
236
|
+
// Belief (T-001): Fireflies marks a finished transcript "processed";
|
|
237
|
+
// "completed" is accepted as the generic terminal spelling. Anything
|
|
238
|
+
// else (including "processing") is honestly in_progress.
|
|
239
|
+
completion_state: (raw.status?.trim().toLowerCase() === "completed" || raw.status?.trim().toLowerCase() === "processed") ? "completed" : "in_progress",
|
|
240
|
+
title: raw.title.trim(),
|
|
241
|
+
participants: firefliesParticipantLabels(raw),
|
|
242
|
+
stable_url: stableUrl,
|
|
243
|
+
// timestamps: Fireflies transcript sentences carry millisecond offsets
|
|
244
|
+
// (belief, T-001), so envelope locators can cite them.
|
|
245
|
+
// open_transcript is observed, not assumed (R-005).
|
|
246
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_access === "granted", stable_link: Boolean(stableUrl), timestamps: true },
|
|
247
|
+
access: "unknown",
|
|
248
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
249
|
+
};
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
// Zoom marks exactly one participant as the account's own: the recording host.
|
|
253
|
+
// The host is the company's rep — no domain label, no contact hint — and every
|
|
254
|
+
// other named participant is treated as external (Gong's internal-party
|
|
255
|
+
// discipline with the only internal marker the recording contract provides).
|
|
256
|
+
// A host name matching nobody leaves every participant external: nothing else
|
|
257
|
+
// is assumed, because Zoom participant records carry no affiliation field.
|
|
258
|
+
function zoomIsHost(raw, displayName) {
|
|
259
|
+
const host = raw.host_display_name?.trim().toLowerCase();
|
|
260
|
+
return Boolean(host) && displayName?.trim().toLowerCase() === host;
|
|
261
|
+
}
|
|
262
|
+
function zoomContactHints(raw) {
|
|
263
|
+
const hints = [];
|
|
264
|
+
for (const participant of raw.participants ?? []) {
|
|
265
|
+
const name = participant.display_name?.trim();
|
|
266
|
+
if (!name || zoomIsHost(raw, name))
|
|
267
|
+
continue;
|
|
268
|
+
hints.push(...contactHintsFromDisplayLabel(name, "zoom:participant"));
|
|
269
|
+
const email = participant.email?.trim();
|
|
270
|
+
if (email) {
|
|
271
|
+
const company = companyFromEmailDomain(email);
|
|
272
|
+
if (company)
|
|
273
|
+
hints.push({ field: "company", value: company, source: "zoom:participant_email_domain" });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const deduped = dedupeContactHints(hints);
|
|
277
|
+
return deduped.length ? deduped : undefined;
|
|
278
|
+
}
|
|
279
|
+
// Participant labels are display names plus, for each non-host participant
|
|
280
|
+
// with an address, a domain-only label ("@exampletonbros.example") — what an inbox
|
|
281
|
+
// scope's domain_allowlist selects on. The local part of the address is
|
|
282
|
+
// discarded here and never leaves the adapter (R-004/R-005).
|
|
283
|
+
function zoomParticipantLabels(raw) {
|
|
284
|
+
const labels = [];
|
|
285
|
+
for (const participant of raw.participants ?? []) {
|
|
286
|
+
const name = participant.display_name?.trim();
|
|
287
|
+
if (name)
|
|
288
|
+
labels.push(name);
|
|
289
|
+
const email = participant.email?.trim().toLowerCase();
|
|
290
|
+
if (email && name && !zoomIsHost(raw, name)) {
|
|
291
|
+
const at = email.lastIndexOf("@");
|
|
292
|
+
if (at > 0 && at < email.length - 1)
|
|
293
|
+
labels.push(`@${email.slice(at + 1)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return [...new Set(labels)];
|
|
297
|
+
}
|
|
298
|
+
// Honest completion: the recording's end when observed, otherwise the meeting
|
|
299
|
+
// start — never a fabricated end (R-005).
|
|
300
|
+
function zoomCompletedAt(raw) {
|
|
301
|
+
return new Date(raw.end_time ?? raw.start_time).toISOString();
|
|
302
|
+
}
|
|
303
|
+
export const zoomDiscoveryAdapter = {
|
|
304
|
+
provider: "zoom",
|
|
305
|
+
normalizeSession(raw, connectionAlias) {
|
|
306
|
+
const contactHints = zoomContactHints(raw);
|
|
307
|
+
// A tokenized or credential-bearing link throws here: refused, never
|
|
308
|
+
// cleaned (R-006). No canonical URL is built from the bare UUID — the
|
|
309
|
+
// account subdomain is unknown to the adapter.
|
|
310
|
+
const stableUrl = raw.url ? sanitizeDiscoveryStableUrl("zoom", raw.url) : undefined;
|
|
311
|
+
return {
|
|
312
|
+
schema_version: "0.1",
|
|
313
|
+
provider: "zoom",
|
|
314
|
+
connection_alias: connectionAlias,
|
|
315
|
+
runtime_handle: raw.meeting_uuid,
|
|
316
|
+
completed_at: zoomCompletedAt(raw),
|
|
317
|
+
completion_state: raw.state === "completed" ? "completed" : "in_progress",
|
|
318
|
+
title: raw.topic.trim(),
|
|
319
|
+
participants: zoomParticipantLabels(raw),
|
|
320
|
+
stable_url: stableUrl,
|
|
321
|
+
// timestamps: VTT cue timing is a belief owned by the feasibility task
|
|
322
|
+
// (T-001); until a real transcript is observed it is not claimed.
|
|
323
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_available === true, stable_link: Boolean(stableUrl), timestamps: false },
|
|
324
|
+
access: "unknown",
|
|
325
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
326
|
+
};
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
function teamsCompletedAt(raw) {
|
|
330
|
+
// Honest completion: an explicit provider end wins; without one the start
|
|
331
|
+
// stands in and the record is not yet completed (the state check below).
|
|
332
|
+
const end = raw.end_time?.trim() || raw.start_time;
|
|
333
|
+
if (!Number.isFinite(Date.parse(end)))
|
|
334
|
+
throw new Error("a Teams meeting needs a parseable start and end time");
|
|
335
|
+
return new Date(end).toISOString();
|
|
336
|
+
}
|
|
337
|
+
// Participants are attendee display names only (R-005): no addresses, no AAD
|
|
338
|
+
// ids, no domain labels — the meeting identifier plus these names are all the
|
|
339
|
+
// shared model carries.
|
|
340
|
+
function teamsParticipants(raw) {
|
|
341
|
+
const names = (raw.attendees ?? []).map((attendee) => attendee.display_name?.trim() ?? "").filter(Boolean);
|
|
342
|
+
return [...new Set(names)];
|
|
343
|
+
}
|
|
344
|
+
// Contact hints are external-only (R-005): the tenant-owned meeting is
|
|
345
|
+
// organized by the company's own staff, so the attendee whose display name
|
|
346
|
+
// matches the organizer is the company's representative and contributes no
|
|
347
|
+
// hint. Every other attendee yields a name hint plus, when the record carries
|
|
348
|
+
// an address, a DOMAIN-only company hint — the address itself dies here
|
|
349
|
+
// (R-004), and free-mail domains yield nothing.
|
|
350
|
+
function teamsContactHints(raw) {
|
|
351
|
+
const organizer = raw.organizer_display_name?.trim().toLowerCase();
|
|
352
|
+
const hints = [];
|
|
353
|
+
for (const attendee of raw.attendees ?? []) {
|
|
354
|
+
const name = attendee.display_name?.trim();
|
|
355
|
+
if (!name)
|
|
356
|
+
continue;
|
|
357
|
+
if (organizer && name.toLowerCase() === organizer)
|
|
358
|
+
continue;
|
|
359
|
+
hints.push(...contactHintsFromDisplayLabel(name, "microsoft-teams:attendee"));
|
|
360
|
+
const email = attendee.email?.trim();
|
|
361
|
+
if (email) {
|
|
362
|
+
const company = companyFromEmailDomain(email);
|
|
363
|
+
if (company)
|
|
364
|
+
hints.push({ field: "company", value: company, source: "microsoft-teams:attendee_email_domain" });
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const deduped = dedupeContactHints(hints);
|
|
368
|
+
return deduped.length ? deduped : undefined;
|
|
369
|
+
}
|
|
370
|
+
export const teamsDiscoveryAdapter = {
|
|
371
|
+
provider: "microsoft-teams",
|
|
372
|
+
normalizeSession(raw, connectionAlias) {
|
|
373
|
+
const contactHints = teamsContactHints(raw);
|
|
374
|
+
// A carried URL is sanitized or the record is refused loudly: with no
|
|
375
|
+
// declared Teams form, any URL (tokenized join links included) throws
|
|
376
|
+
// here — never partially cleaned (R-006).
|
|
377
|
+
const stableUrl = raw.url ? sanitizeDiscoveryStableUrl("microsoft-teams", raw.url) : undefined;
|
|
378
|
+
return {
|
|
379
|
+
schema_version: "0.1",
|
|
380
|
+
provider: "microsoft-teams",
|
|
381
|
+
connection_alias: connectionAlias,
|
|
382
|
+
runtime_handle: raw.meeting_id,
|
|
383
|
+
completed_at: teamsCompletedAt(raw),
|
|
384
|
+
completion_state: raw.state === "completed" ? "completed" : "in_progress",
|
|
385
|
+
title: raw.subject.trim(),
|
|
386
|
+
participants: teamsParticipants(raw),
|
|
387
|
+
stable_url: stableUrl,
|
|
388
|
+
// timestamps: the Graph transcript's locator capability is unverified
|
|
389
|
+
// (T-001 owns it), so the capability fails closed until observed.
|
|
390
|
+
// open_transcript is observed, never assumed (R-005).
|
|
391
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_available === true, stable_link: Boolean(stableUrl), timestamps: false },
|
|
392
|
+
access: "unknown",
|
|
393
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
export const fakeDiscoveryAdapter = {
|
|
398
|
+
provider: "fakecalls",
|
|
399
|
+
normalizeSession(raw, connectionAlias) {
|
|
400
|
+
return {
|
|
401
|
+
schema_version: "0.1",
|
|
402
|
+
provider: "fakecalls",
|
|
403
|
+
connection_alias: connectionAlias,
|
|
404
|
+
runtime_handle: raw.key,
|
|
405
|
+
completed_at: new Date(raw.ended).toISOString(),
|
|
406
|
+
completion_state: raw.done ? "completed" : "in_progress",
|
|
407
|
+
title: raw.subject.trim(),
|
|
408
|
+
participants: raw.people.labels.map((label) => label.trim()).filter(Boolean),
|
|
409
|
+
stable_url: raw.review_link ? sanitizeDiscoveryStableUrl("fakecalls", raw.review_link) : undefined,
|
|
410
|
+
capabilities: { list_sessions: true, open_transcript: raw.transcript_available === true, stable_link: Boolean(raw.review_link), timestamps: false },
|
|
411
|
+
access: "unknown",
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
const ZENDESK_COMPLETED_STATUSES = new Set(["solved", "closed"]);
|
|
416
|
+
// Participant labels are display names plus, for the external requester with
|
|
417
|
+
// an address, a domain-only label ("@exampletonbros.example") — the label an inbox
|
|
418
|
+
// scope's domain_allowlist selects on. The assignee is the company's own
|
|
419
|
+
// agent and contributes no domain label. The local part of the requester
|
|
420
|
+
// address is discarded here and never leaves the adapter (R-004/R-005).
|
|
421
|
+
function zendeskParticipantLabels(raw) {
|
|
422
|
+
const labels = [];
|
|
423
|
+
const requesterName = raw.requester?.display_name?.trim();
|
|
424
|
+
if (requesterName)
|
|
425
|
+
labels.push(requesterName);
|
|
426
|
+
const email = raw.requester?.email?.trim().toLowerCase();
|
|
427
|
+
const at = email ? email.lastIndexOf("@") : -1;
|
|
428
|
+
if (email && at > 0 && at < email.length - 1)
|
|
429
|
+
labels.push(`@${email.slice(at + 1)}`);
|
|
430
|
+
const assigneeName = raw.assignee_display_name?.trim();
|
|
431
|
+
if (assigneeName)
|
|
432
|
+
labels.push(assigneeName);
|
|
433
|
+
return [...new Set(labels)];
|
|
434
|
+
}
|
|
435
|
+
// Contact hints follow the Granola discipline (discovery-contact-capture-v0
|
|
436
|
+
// R-003/R-004) with the support-class rule: only the requester is a customer
|
|
437
|
+
// contact candidate — the assignee is company staff, and the raw address dies
|
|
438
|
+
// here, replaced by its domain.
|
|
439
|
+
function zendeskContactHints(raw) {
|
|
440
|
+
const hints = [];
|
|
441
|
+
const name = raw.requester?.display_name?.trim();
|
|
442
|
+
if (name)
|
|
443
|
+
hints.push(...contactHintsFromDisplayLabel(name, "zendesk:requester"));
|
|
444
|
+
const email = raw.requester?.email?.trim();
|
|
445
|
+
if (email) {
|
|
446
|
+
const company = companyFromEmailDomain(email);
|
|
447
|
+
if (company)
|
|
448
|
+
hints.push({ field: "company", value: company, source: "zendesk:requester_email_domain" });
|
|
449
|
+
}
|
|
450
|
+
const deduped = dedupeContactHints(hints);
|
|
451
|
+
return deduped.length ? deduped : undefined;
|
|
452
|
+
}
|
|
453
|
+
// D-001's explicit done-label override (T-005): a thread carrying any of these
|
|
454
|
+
// labels is completed evidence regardless of recency. Kept beside the email
|
|
455
|
+
// class rather than the transport policy — it is class semantics every Gmail
|
|
456
|
+
// runtime shares, versioned with the class, not a request ceiling.
|
|
457
|
+
export const GMAIL_DONE_LABELS = ["done"];
|
|
458
|
+
// D-001 (gmail-discovery-connector-v0 T-005, pinned per OQ-001's
|
|
459
|
+
// recommendation): a thread is "completed" evidence when its last message
|
|
460
|
+
// predates the approved scope's quiet-period boundary, or when it carries an
|
|
461
|
+
// explicit done label. Without a stamped boundary and without a done label the
|
|
462
|
+
// thread is not settled — in_progress, never guessed.
|
|
463
|
+
export function gmailThreadAdmissibility(raw) {
|
|
464
|
+
const labels = new Set((raw.labels ?? []).map((label) => label.trim().toLowerCase()).filter(Boolean));
|
|
465
|
+
if (GMAIL_DONE_LABELS.some((label) => labels.has(label)))
|
|
466
|
+
return "completed";
|
|
467
|
+
const boundary = Date.parse(raw.quiet_period_ends_at ?? "");
|
|
468
|
+
const last = Date.parse(raw.last_message_at);
|
|
469
|
+
if (Number.isFinite(boundary) && Number.isFinite(last) && last < boundary)
|
|
470
|
+
return "completed";
|
|
471
|
+
return "in_progress";
|
|
472
|
+
}
|
|
473
|
+
function gmailDomainLabel(email) {
|
|
474
|
+
// Domain labels exist so an inbox scope's domain_allowlist can select on
|
|
475
|
+
// them (the raw address never leaves the adapter, R-005). Free-mail domains
|
|
476
|
+
// yield no label: "@gmail.com" is noise for a customer selector, and the
|
|
477
|
+
// shared rule already refuses to derive an employer from them.
|
|
478
|
+
if (!companyFromEmailDomain(email))
|
|
479
|
+
return undefined;
|
|
480
|
+
return `@${email.slice(email.lastIndexOf("@") + 1).trim().toLowerCase()}`;
|
|
481
|
+
}
|
|
482
|
+
function internalDomain(raw) {
|
|
483
|
+
const owner = raw.mailbox_owner_email?.trim().toLowerCase();
|
|
484
|
+
const at = owner ? owner.lastIndexOf("@") : -1;
|
|
485
|
+
return owner && at > 0 && at < owner.length - 1 ? owner.slice(at + 1) : undefined;
|
|
486
|
+
}
|
|
487
|
+
// R-004: a thread with no participant outside the mailbox owner's own domain
|
|
488
|
+
// is the company talking to itself in one person's mailbox and is excluded by
|
|
489
|
+
// default. A thread whose participants carry no addresses stays in — the
|
|
490
|
+
// scope's own selectors still have to admit it.
|
|
491
|
+
export function gmailInternalOnly(raw) {
|
|
492
|
+
const own = internalDomain(raw);
|
|
493
|
+
if (!own)
|
|
494
|
+
return false;
|
|
495
|
+
const addressed = (raw.participants ?? []).filter((participant) => participant.email?.trim());
|
|
496
|
+
if (addressed.length === 0)
|
|
497
|
+
return false;
|
|
498
|
+
return addressed.every((participant) => participant.email.trim().toLowerCase().endsWith(`@${own}`));
|
|
499
|
+
}
|
|
500
|
+
// Contact hints follow the shared discipline: display names become full_name
|
|
501
|
+
// candidates, a non-free email DOMAIN becomes the company, and the address
|
|
502
|
+
// itself dies here. The mailbox owner is the company's own rep — their
|
|
503
|
+
// identity contributes no hint and no domain label (per-user ownership,
|
|
504
|
+
// D-002).
|
|
505
|
+
function gmailContactHints(raw) {
|
|
506
|
+
const own = internalDomain(raw);
|
|
507
|
+
const hints = [];
|
|
508
|
+
for (const participant of raw.participants ?? []) {
|
|
509
|
+
const name = participant.display_name?.trim();
|
|
510
|
+
const email = participant.email?.trim();
|
|
511
|
+
if (own && email?.toLowerCase().endsWith(`@${own}`))
|
|
512
|
+
continue;
|
|
513
|
+
if (name)
|
|
514
|
+
hints.push(...contactHintsFromDisplayLabel(name, "gmail:participant"));
|
|
515
|
+
if (email) {
|
|
516
|
+
const company = companyFromEmailDomain(email);
|
|
517
|
+
if (company)
|
|
518
|
+
hints.push({ field: "company", value: company, source: "gmail:participant_email_domain" });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const deduped = dedupeContactHints(hints);
|
|
522
|
+
return deduped.length ? deduped : undefined;
|
|
523
|
+
}
|
|
524
|
+
export const zendeskDiscoveryAdapter = {
|
|
525
|
+
provider: "zendesk",
|
|
526
|
+
normalizeSession(raw, connectionAlias) {
|
|
527
|
+
// A solved-class ticket with no observed solved time stays in_progress
|
|
528
|
+
// rather than inventing a completion instant (D-001).
|
|
529
|
+
const solvedAt = raw.solved_at?.trim();
|
|
530
|
+
const stableUrl = raw.url ? sanitizeDiscoveryStableUrl("zendesk", raw.url) : undefined;
|
|
531
|
+
const contactHints = zendeskContactHints(raw);
|
|
532
|
+
return {
|
|
533
|
+
schema_version: "0.1",
|
|
534
|
+
provider: "zendesk",
|
|
535
|
+
connection_alias: connectionAlias,
|
|
536
|
+
runtime_handle: raw.id,
|
|
537
|
+
completed_at: solvedAt ? new Date(solvedAt).toISOString() : "",
|
|
538
|
+
completion_state: ZENDESK_COMPLETED_STATUSES.has(raw.status.toLowerCase()) && solvedAt ? "completed" : "in_progress",
|
|
539
|
+
title: raw.subject.trim(),
|
|
540
|
+
participants: zendeskParticipantLabels(raw),
|
|
541
|
+
stable_url: stableUrl,
|
|
542
|
+
// Comment threads carry a created_at per comment, so the timestamps
|
|
543
|
+
// capability rides the same observed grant as open_transcript (R-005):
|
|
544
|
+
// neither is claimed without the connection-level observation.
|
|
545
|
+
capabilities: { list_sessions: true, open_transcript: raw.comment_access === "granted", stable_link: Boolean(stableUrl), timestamps: raw.comment_access === "granted" },
|
|
546
|
+
access: "unknown",
|
|
547
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
548
|
+
};
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
// Participant labels are display names plus, for each non-free-mail address,
|
|
552
|
+
// a domain-only label ("@exampletonbros.example"). The local part of every address
|
|
553
|
+
// is discarded here and never leaves the adapter (R-005).
|
|
554
|
+
function gmailParticipantLabels(raw) {
|
|
555
|
+
const own = internalDomain(raw);
|
|
556
|
+
const labels = [];
|
|
557
|
+
for (const participant of raw.participants ?? []) {
|
|
558
|
+
const name = participant.display_name?.trim();
|
|
559
|
+
if (name)
|
|
560
|
+
labels.push(name);
|
|
561
|
+
const email = participant.email?.trim().toLowerCase();
|
|
562
|
+
if (!email || (own && email.endsWith(`@${own}`)))
|
|
563
|
+
continue;
|
|
564
|
+
const domain = gmailDomainLabel(email);
|
|
565
|
+
if (domain)
|
|
566
|
+
labels.push(domain);
|
|
567
|
+
}
|
|
568
|
+
return [...new Set(labels)];
|
|
569
|
+
}
|
|
570
|
+
export const gmailDiscoveryAdapter = {
|
|
571
|
+
provider: "gmail",
|
|
572
|
+
normalizeSession(raw, connectionAlias) {
|
|
573
|
+
const contactHints = gmailContactHints(raw);
|
|
574
|
+
// R-006: Gmail declares no ProviderLinkForm in V0 — the believed web
|
|
575
|
+
// thread URL carries the identifier in a fragment (or query) form that the
|
|
576
|
+
// global stable-link rule refuses, and the feasibility task owns verifying
|
|
577
|
+
// the real forms. Links stay unset with a coverage caveat; a tokenized or
|
|
578
|
+
// authenticated URL is never stored. The raw url field dies here.
|
|
579
|
+
void raw.url;
|
|
580
|
+
return {
|
|
581
|
+
schema_version: "0.1",
|
|
582
|
+
provider: "gmail",
|
|
583
|
+
connection_alias: connectionAlias,
|
|
584
|
+
runtime_handle: raw.thread_id,
|
|
585
|
+
completed_at: new Date(raw.last_message_at).toISOString(),
|
|
586
|
+
completion_state: gmailThreadAdmissibility(raw),
|
|
587
|
+
title: raw.snippet_subject.trim() || "(no subject)",
|
|
588
|
+
participants: gmailParticipantLabels(raw),
|
|
589
|
+
// No declared link form → no stable link, and sanitizeDiscoveryStableUrl
|
|
590
|
+
// keeps refusing every Gmail URL shape (query or fragment) unchanged.
|
|
591
|
+
stable_url: undefined,
|
|
592
|
+
// timestamps: email messages carry no per-locus timestamps an envelope
|
|
593
|
+
// locator could cite (the locator format is mm:ss of a recording), so
|
|
594
|
+
// this stays honestly false. open_transcript (body access) is observed,
|
|
595
|
+
// not assumed (R-005).
|
|
596
|
+
capabilities: { list_sessions: true, open_transcript: raw.body_access === "granted", stable_link: false, timestamps: false },
|
|
597
|
+
access: "unknown",
|
|
598
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
599
|
+
};
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
export const slackDiscoveryAdapter = {
|
|
603
|
+
provider: "slack",
|
|
604
|
+
normalizeSession(raw, connectionAlias) {
|
|
605
|
+
const stableUrl = raw.permalink ? sanitizeDiscoveryStableUrl("slack", raw.permalink) : undefined;
|
|
606
|
+
const lastMessageSeconds = raw.last_message_at ? Number(raw.last_message_at.split(".")[0]) : undefined;
|
|
607
|
+
const fallbackSeconds = Number(raw.thread_ts.split(".")[0]);
|
|
608
|
+
return {
|
|
609
|
+
schema_version: "0.1",
|
|
610
|
+
provider: "slack",
|
|
611
|
+
connection_alias: connectionAlias,
|
|
612
|
+
runtime_handle: `${raw.channel_id}:${raw.thread_ts}`,
|
|
613
|
+
// A thread is complete by selection: processing reads exactly the thread
|
|
614
|
+
// the scope named. Truncation inside a read is recorded on the envelope
|
|
615
|
+
// (transcript_complete), not by pretending the conversation is unfinished.
|
|
616
|
+
completed_at: raw.last_message_at && lastMessageSeconds
|
|
617
|
+
? new Date(lastMessageSeconds * 1000).toISOString()
|
|
618
|
+
: new Date(fallbackSeconds * 1000).toISOString(),
|
|
619
|
+
completion_state: "completed",
|
|
620
|
+
// Channel-level label (D-001): the disclosure level for chat evidence is
|
|
621
|
+
// the channel, never a speaker, so participants stay empty and the title
|
|
622
|
+
// names the channel or the selector — not people.
|
|
623
|
+
title: raw.channel_name?.trim() ? `Slack thread in #${raw.channel_name.trim()}` : `Slack thread ${raw.channel_id} ${raw.thread_ts}`,
|
|
624
|
+
participants: [],
|
|
625
|
+
stable_url: stableUrl,
|
|
626
|
+
capabilities: { list_sessions: true, open_transcript: true, stable_link: Boolean(stableUrl), timestamps: true },
|
|
627
|
+
access: "unknown",
|
|
628
|
+
};
|
|
629
|
+
},
|
|
630
|
+
};
|
|
631
|
+
// The email class's explicit done override (T-005), adapted to conversation
|
|
632
|
+
// groups: Outlook has no label store, so the override maps Gmail's done label
|
|
633
|
+
// onto Graph message categories — the closest per-message marker a mailbox
|
|
634
|
+
// owner sets by hand. Same value list as GMAIL_DONE_LABELS; same class
|
|
635
|
+
// semantics (a category marker is intent, not transport policy).
|
|
636
|
+
export const OUTLOOK_DONE_CATEGORIES = ["done"];
|
|
637
|
+
// The email class's admissibility rule (outlook-discovery-connector-v0 T-005,
|
|
638
|
+
// pinned as the Gmail packet's D-001 rule applied to conversation groups): a
|
|
639
|
+
// conversation is "completed" evidence when its last message predates the
|
|
640
|
+
// approved scope's quiet-period boundary, or when a message in the group
|
|
641
|
+
// carries an explicit done category. Without a stamped boundary and without a
|
|
642
|
+
// done category the conversation is not settled — in_progress, never guessed.
|
|
643
|
+
export function outlookConversationAdmissibility(raw) {
|
|
644
|
+
const categories = new Set((raw.categories ?? []).map((category) => category.trim().toLowerCase()).filter(Boolean));
|
|
645
|
+
if (OUTLOOK_DONE_CATEGORIES.some((category) => categories.has(category)))
|
|
646
|
+
return "completed";
|
|
647
|
+
const boundary = Date.parse(raw.quiet_period_ends_at ?? "");
|
|
648
|
+
const last = Date.parse(raw.last_message_at);
|
|
649
|
+
if (Number.isFinite(boundary) && Number.isFinite(last) && last < boundary)
|
|
650
|
+
return "completed";
|
|
651
|
+
return "in_progress";
|
|
652
|
+
}
|
|
653
|
+
function outlookDomainLabel(smtp) {
|
|
654
|
+
// Same discipline as Gmail's: domain labels exist so an inbox scope's
|
|
655
|
+
// domain_allowlist can select on them (the raw address never leaves the
|
|
656
|
+
// adapter, R-005), and free-mail domains yield no label — "@outlook.com" is
|
|
657
|
+
// noise for a customer selector, and the shared rule already refuses to
|
|
658
|
+
// derive an employer from free-mail domains.
|
|
659
|
+
if (!companyFromEmailDomain(smtp))
|
|
660
|
+
return undefined;
|
|
661
|
+
return `@${smtp.slice(smtp.lastIndexOf("@") + 1).trim().toLowerCase()}`;
|
|
662
|
+
}
|
|
663
|
+
function outlookInternalDomain(raw) {
|
|
664
|
+
const owner = raw.mailbox_owner_email?.trim().toLowerCase();
|
|
665
|
+
const at = owner ? owner.lastIndexOf("@") : -1;
|
|
666
|
+
return owner && at > 0 && at < owner.length - 1 ? owner.slice(at + 1) : undefined;
|
|
667
|
+
}
|
|
668
|
+
// R-004: a conversation with no participant outside the mailbox owner's own
|
|
669
|
+
// domain is the company talking to itself in one person's mailbox and is
|
|
670
|
+
// excluded by default. A conversation whose participants carry no smtp
|
|
671
|
+
// addresses stays in — the scope's own selectors still have to admit it.
|
|
672
|
+
export function outlookInternalOnly(raw) {
|
|
673
|
+
const own = outlookInternalDomain(raw);
|
|
674
|
+
if (!own)
|
|
675
|
+
return false;
|
|
676
|
+
const addressed = (raw.participants ?? []).filter((participant) => participant.smtp?.trim());
|
|
677
|
+
if (addressed.length === 0)
|
|
678
|
+
return false;
|
|
679
|
+
return addressed.every((participant) => participant.smtp.trim().toLowerCase().endsWith(`@${own}`));
|
|
680
|
+
}
|
|
681
|
+
// Contact hints follow the shared discipline: display names become full_name
|
|
682
|
+
// candidates, a non-free smtp DOMAIN becomes the company, and the address
|
|
683
|
+
// itself dies here. The mailbox owner is the company's own rep — their
|
|
684
|
+
// identity contributes no hint and no domain label (per-user ownership,
|
|
685
|
+
// D-002).
|
|
686
|
+
function outlookContactHints(raw) {
|
|
687
|
+
const own = outlookInternalDomain(raw);
|
|
688
|
+
const hints = [];
|
|
689
|
+
for (const participant of raw.participants ?? []) {
|
|
690
|
+
const name = participant.display_name?.trim();
|
|
691
|
+
const smtp = participant.smtp?.trim();
|
|
692
|
+
if (own && smtp?.toLowerCase().endsWith(`@${own}`))
|
|
693
|
+
continue;
|
|
694
|
+
if (name)
|
|
695
|
+
hints.push(...contactHintsFromDisplayLabel(name, "outlook:participant"));
|
|
696
|
+
if (smtp) {
|
|
697
|
+
const company = companyFromEmailDomain(smtp);
|
|
698
|
+
if (company)
|
|
699
|
+
hints.push({ field: "company", value: company, source: "outlook:participant_smtp_domain" });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
const deduped = dedupeContactHints(hints);
|
|
703
|
+
return deduped.length ? deduped : undefined;
|
|
704
|
+
}
|
|
705
|
+
// Participant labels are display names plus, for each non-free-mail address,
|
|
706
|
+
// a domain-only label ("@exampletonbros.example"). The local part of every address
|
|
707
|
+
// is discarded here and never leaves the adapter (R-005).
|
|
708
|
+
function outlookParticipantLabels(raw) {
|
|
709
|
+
const own = outlookInternalDomain(raw);
|
|
710
|
+
const labels = [];
|
|
711
|
+
for (const participant of raw.participants ?? []) {
|
|
712
|
+
const name = participant.display_name?.trim();
|
|
713
|
+
if (name)
|
|
714
|
+
labels.push(name);
|
|
715
|
+
const smtp = participant.smtp?.trim().toLowerCase();
|
|
716
|
+
if (!smtp || (own && smtp.endsWith(`@${own}`)))
|
|
717
|
+
continue;
|
|
718
|
+
const domain = outlookDomainLabel(smtp);
|
|
719
|
+
if (domain)
|
|
720
|
+
labels.push(domain);
|
|
721
|
+
}
|
|
722
|
+
return [...new Set(labels)];
|
|
723
|
+
}
|
|
724
|
+
export const outlookDiscoveryAdapter = {
|
|
725
|
+
provider: "outlook",
|
|
726
|
+
normalizeSession(raw, connectionAlias) {
|
|
727
|
+
const contactHints = outlookContactHints(raw);
|
|
728
|
+
// R-006: Outlook declares no ProviderLinkForm in V0 — Graph web links
|
|
729
|
+
// embed the item id as an opaque token in the path (or carry it in a
|
|
730
|
+
// query), a form the global stable-link rule refuses, and the feasibility
|
|
731
|
+
// task owns verifying the real forms. Links stay unset; a tokenized or
|
|
732
|
+
// authenticated URL is never stored. The raw url field dies here.
|
|
733
|
+
void raw.url;
|
|
734
|
+
return {
|
|
735
|
+
schema_version: "0.1",
|
|
736
|
+
provider: "outlook",
|
|
737
|
+
connection_alias: connectionAlias,
|
|
738
|
+
runtime_handle: raw.conversation_id,
|
|
739
|
+
completed_at: new Date(raw.last_message_at).toISOString(),
|
|
740
|
+
completion_state: outlookConversationAdmissibility(raw),
|
|
741
|
+
title: raw.subject.trim() || "(no subject)",
|
|
742
|
+
participants: outlookParticipantLabels(raw),
|
|
743
|
+
// No declared link form → no stable link, and
|
|
744
|
+
// sanitizeDiscoveryStableUrl keeps refusing every Outlook web-link
|
|
745
|
+
// shape (tokenized path or query) unchanged.
|
|
746
|
+
stable_url: undefined,
|
|
747
|
+
// timestamps: email messages carry no per-locus timestamps an envelope
|
|
748
|
+
// locator could cite (the locator format is mm:ss of a recording), so
|
|
749
|
+
// this stays honestly false. open_transcript (body access) is observed,
|
|
750
|
+
// not assumed (R-005).
|
|
751
|
+
capabilities: { list_sessions: true, open_transcript: raw.body_access === "granted", stable_link: false, timestamps: false },
|
|
752
|
+
access: "unknown",
|
|
753
|
+
...(contactHints ? { contact_hints: contactHints } : {}),
|
|
754
|
+
};
|
|
755
|
+
},
|
|
756
|
+
};
|