← back to Cli Printing Press
feat(generator): add Non-Obvious Insight system, domain archetype detection, and entity mapping
f5369dd0bfebb99a665f0e23244a88741919f098 · 2026-03-26 09:53:40 -0700 · Matt Van Horn
Phases 0-2 of the Creative Vision Engine:
- NOI System: NonObviousInsight struct in vision/insight.go with Formula() and HasInsight()
- Domain Archetypes: DomainSignals detection in profiler (communication, PM, payments,
infrastructure, content, CRM, developer-platform) via keyword heuristics on resource
names and field names
- Entity Mapper: WorkflowTemplateContext + EntityMapping in generator/entity_mapper.go
that maps API spec resources to their domain roles (primary entity, team, user, state)
- VisionTemplateSet: Workflows and Insights slice fields for conditional template rendering
- NOI Examples: Reference file with 10+ examples across all archetypes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Files touched
A internal/generator/entity_mapper.goM internal/generator/generator.goM internal/generator/vision_templates.goM internal/profiler/profiler.goA internal/vision/insight.goM internal/vision/vision.goA skills/printing-press/references/noi-examples.md
Diff
commit f5369dd0bfebb99a665f0e23244a88741919f098
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date: Thu Mar 26 09:53:40 2026 -0700
feat(generator): add Non-Obvious Insight system, domain archetype detection, and entity mapping
Phases 0-2 of the Creative Vision Engine:
- NOI System: NonObviousInsight struct in vision/insight.go with Formula() and HasInsight()
- Domain Archetypes: DomainSignals detection in profiler (communication, PM, payments,
infrastructure, content, CRM, developer-platform) via keyword heuristics on resource
names and field names
- Entity Mapper: WorkflowTemplateContext + EntityMapping in generator/entity_mapper.go
that maps API spec resources to their domain roles (primary entity, team, user, state)
- VisionTemplateSet: Workflows and Insights slice fields for conditional template rendering
- NOI Examples: Reference file with 10+ examples across all archetypes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---
internal/generator/entity_mapper.go | 203 +++++++++++++++++++++++
internal/generator/generator.go | 2 +-
internal/generator/vision_templates.go | 35 ++++
internal/profiler/profiler.go | 148 +++++++++++++++++
internal/vision/insight.go | 19 +++
internal/vision/vision.go | 12 ++
skills/printing-press/references/noi-examples.md | 49 ++++++
7 files changed, 467 insertions(+), 1 deletion(-)
diff --git a/internal/generator/entity_mapper.go b/internal/generator/entity_mapper.go
new file mode 100644
index 00000000..3b8840ab
--- /dev/null
+++ b/internal/generator/entity_mapper.go
@@ -0,0 +1,203 @@
+package generator
+
+import (
+ "strings"
+
+ "github.com/mvanhorn/cli-printing-press/internal/profiler"
+ "github.com/mvanhorn/cli-printing-press/internal/spec"
+)
+
+type WorkflowTemplateContext struct {
+ CLIName string
+ APIName string
+ Domain string
+ PrimaryEntity EntityMapping
+ TeamEntity EntityMapping
+ UserEntity EntityMapping
+ StateEntity EntityMapping
+ HasAssignees bool
+ HasDueDates bool
+ HasPriority bool
+ HasTeams bool
+ HasLabels bool
+ HasEstimates bool
+}
+
+type EntityMapping struct {
+ TableName string
+ HumanSingular string
+ HumanPlural string
+ IdentifierField string
+ TitleField string
+ UpdatedAtField string
+ AssigneeField string
+ PriorityField string
+ DueDateField string
+ TeamField string
+ LabelField string
+ EstimateField string
+ StateField string
+}
+
+func MapEntities(s *spec.APISpec, domain profiler.DomainSignals) WorkflowTemplateContext {
+ ctx := WorkflowTemplateContext{
+ Domain: string(domain.Archetype),
+ HasAssignees: domain.HasAssignees,
+ HasDueDates: domain.HasDueDates,
+ HasPriority: domain.HasPriority,
+ HasTeams: domain.HasTeams,
+ HasLabels: domain.HasLabels,
+ HasEstimates: domain.HasEstimates,
+ }
+
+ if s == nil {
+ return ctx
+ }
+
+ ctx.APIName = s.Name
+
+ primaryKeywords := primaryKeywordsForArchetype(domain.Archetype)
+ teamKeywords := []string{"team", "group", "organization", "workspace"}
+ userKeywords := []string{"user", "member", "person", "contact", "account"}
+ stateKeywords := []string{"status", "state", "stage", "phase"}
+
+ for name, resource := range s.Resources {
+ nameLower := strings.ToLower(name)
+
+ if ctx.PrimaryEntity.TableName == "" && matchesKeywords(nameLower, primaryKeywords) {
+ ctx.PrimaryEntity = mapResource(name, resource)
+ }
+ if ctx.TeamEntity.TableName == "" && matchesKeywords(nameLower, teamKeywords) {
+ ctx.TeamEntity = mapResource(name, resource)
+ }
+ if ctx.UserEntity.TableName == "" && matchesKeywords(nameLower, userKeywords) {
+ ctx.UserEntity = mapResource(name, resource)
+ }
+ if ctx.StateEntity.TableName == "" && matchesKeywords(nameLower, stateKeywords) {
+ ctx.StateEntity = mapResource(name, resource)
+ }
+
+ for subName, sub := range resource.SubResources {
+ subLower := strings.ToLower(subName)
+ if ctx.PrimaryEntity.TableName == "" && matchesKeywords(subLower, primaryKeywords) {
+ ctx.PrimaryEntity = mapResource(subName, sub)
+ }
+ }
+ }
+
+ if ctx.PrimaryEntity.TableName != "" {
+ scanEntityFields(s, ctx.PrimaryEntity.TableName, &ctx.PrimaryEntity)
+ }
+
+ return ctx
+}
+
+func primaryKeywordsForArchetype(archetype profiler.DomainArchetype) []string {
+ switch archetype {
+ case profiler.ArchetypeProjectMgmt:
+ return []string{"issue", "task", "ticket", "work_item", "story"}
+ case profiler.ArchetypeCommunication:
+ return []string{"message", "chat", "conversation"}
+ case profiler.ArchetypePayments:
+ return []string{"charge", "payment", "invoice", "transaction"}
+ case profiler.ArchetypeInfrastructure:
+ return []string{"instance", "server", "deployment", "container"}
+ case profiler.ArchetypeContent:
+ return []string{"article", "post", "page", "document"}
+ case profiler.ArchetypeCRM:
+ return []string{"contact", "deal", "lead", "opportunity"}
+ case profiler.ArchetypeDeveloperPlatform:
+ return []string{"repository", "pull_request", "merge_request", "commit"}
+ default:
+ return []string{}
+ }
+}
+
+func matchesKeywords(name string, keywords []string) bool {
+ for _, kw := range keywords {
+ if strings.Contains(name, kw) {
+ return true
+ }
+ }
+ return false
+}
+
+func mapResource(name string, r spec.Resource) EntityMapping {
+ singular := strings.ToLower(name)
+ singular = strings.TrimSuffix(singular, "s")
+
+ plural := singular + "s"
+ if strings.HasSuffix(singular, "y") && !strings.HasSuffix(singular, "ey") {
+ plural = singular[:len(singular)-1] + "ies"
+ }
+
+ em := EntityMapping{
+ TableName: strings.ReplaceAll(strings.ToLower(name), "-", "_"),
+ HumanSingular: singular,
+ HumanPlural: plural,
+ }
+
+ for _, endpoint := range r.Endpoints {
+ scanMappingFields(endpoint.Body, &em)
+ scanMappingFields(endpoint.Params, &em)
+ }
+
+ if em.IdentifierField == "" {
+ em.IdentifierField = "id"
+ }
+
+ return em
+}
+
+func scanMappingFields(params []spec.Param, em *EntityMapping) {
+ for _, p := range params {
+ name := strings.ToLower(p.Name)
+
+ if em.IdentifierField == "" && (name == "id" || strings.HasSuffix(name, "_id")) {
+ em.IdentifierField = p.Name
+ }
+ if em.TitleField == "" && (name == "title" || name == "name" || name == "subject" || name == "summary") {
+ em.TitleField = p.Name
+ }
+ if em.UpdatedAtField == "" && (name == "updated_at" || name == "modified_at" || name == "last_modified") {
+ em.UpdatedAtField = p.Name
+ }
+ if em.AssigneeField == "" && (strings.Contains(name, "assignee") || name == "assigned_to") {
+ em.AssigneeField = p.Name
+ }
+ if em.PriorityField == "" && strings.Contains(name, "priority") {
+ em.PriorityField = p.Name
+ }
+ if em.DueDateField == "" && (strings.Contains(name, "due_date") || strings.Contains(name, "due_at") || strings.Contains(name, "deadline")) {
+ em.DueDateField = p.Name
+ }
+ if em.TeamField == "" && (name == "team_id" || name == "team") {
+ em.TeamField = p.Name
+ }
+ if em.LabelField == "" && (strings.Contains(name, "label") || name == "tags") {
+ em.LabelField = p.Name
+ }
+ if em.EstimateField == "" && (strings.Contains(name, "estimate") || strings.Contains(name, "story_points") || name == "points") {
+ em.EstimateField = p.Name
+ }
+ if em.StateField == "" && (name == "status" || name == "state") {
+ em.StateField = p.Name
+ }
+
+ if len(p.Fields) > 0 {
+ scanMappingFields(p.Fields, em)
+ }
+ }
+}
+
+func scanEntityFields(s *spec.APISpec, tableName string, em *EntityMapping) {
+ for name, resource := range s.Resources {
+ if strings.EqualFold(strings.ReplaceAll(strings.ToLower(name), "-", "_"), tableName) {
+ for _, endpoint := range resource.Endpoints {
+ scanMappingFields(endpoint.Body, em)
+ scanMappingFields(endpoint.Params, em)
+ }
+ return
+ }
+ }
+}
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 9a75decf..bb32848b 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -191,7 +191,7 @@ func (g *Generator) Generate() error {
}
// Vision features: profile the API and render selected templates
- if g.VisionSet == (VisionTemplateSet{}) {
+ if g.VisionSet.IsZero() {
// Auto-profile if no explicit vision set provided
g.profile = profiler.Profile(g.Spec)
plan := g.profile.ToVisionaryPlan(g.Spec.Name)
diff --git a/internal/generator/vision_templates.go b/internal/generator/vision_templates.go
index 1200787c..56cbf2dd 100644
--- a/internal/generator/vision_templates.go
+++ b/internal/generator/vision_templates.go
@@ -13,6 +13,14 @@ type VisionTemplateSet struct {
Sync bool
Tail bool
Analytics bool
+ Workflows []string
+ Insights []string
+}
+
+func (s VisionTemplateSet) IsZero() bool {
+ return !s.Export && !s.Import && !s.Store && !s.Search &&
+ !s.Sync && !s.Tail && !s.Analytics &&
+ len(s.Workflows) == 0 && len(s.Insights) == 0
}
// SelectVisionTemplates determines which domain-aware templates to include
@@ -91,9 +99,34 @@ func SelectVisionTemplates(plan *vision.VisionaryPlan) VisionTemplateSet {
}
}
+ switch plan.Domain.Archetype {
+ case "project-management":
+ set.Workflows = []string{
+ "workflows/pm_stale.go.tmpl",
+ "workflows/pm_orphans.go.tmpl",
+ "workflows/pm_load.go.tmpl",
+ }
+ case "communication":
+ set.Workflows = []string{
+ "workflows/comm_health.go.tmpl",
+ }
+ }
+
+ if plan.Insight.HasInsight() {
+ set.Insights = []string{"insight_report.go.tmpl"}
+ }
+
return set
}
+func (s VisionTemplateSet) HasWorkflows() bool {
+ return len(s.Workflows) > 0
+}
+
+func (s VisionTemplateSet) HasInsights() bool {
+ return len(s.Insights) > 0
+}
+
// TemplateNames returns the list of template filenames to render.
func (s VisionTemplateSet) TemplateNames() []string {
var names []string
@@ -118,5 +151,7 @@ func (s VisionTemplateSet) TemplateNames() []string {
if s.Analytics {
names = append(names, "analytics.go.tmpl")
}
+ names = append(names, s.Workflows...)
+ names = append(names, s.Insights...)
return names
}
diff --git a/internal/profiler/profiler.go b/internal/profiler/profiler.go
index 472278b6..88725188 100644
--- a/internal/profiler/profiler.go
+++ b/internal/profiler/profiler.go
@@ -8,6 +8,33 @@ import (
"github.com/mvanhorn/cli-printing-press/internal/vision"
)
+type DomainArchetype string
+
+const (
+ ArchetypeCommunication DomainArchetype = "communication"
+ ArchetypeProjectMgmt DomainArchetype = "project-management"
+ ArchetypePayments DomainArchetype = "payments"
+ ArchetypeInfrastructure DomainArchetype = "infrastructure"
+ ArchetypeContent DomainArchetype = "content"
+ ArchetypeCRM DomainArchetype = "crm"
+ ArchetypeDeveloperPlatform DomainArchetype = "developer-platform"
+ ArchetypeGeneric DomainArchetype = "generic"
+)
+
+type DomainSignals struct {
+ Archetype DomainArchetype
+ HasAssignees bool
+ HasDueDates bool
+ HasPriority bool
+ HasThreading bool
+ HasTransactions bool
+ HasSubscriptions bool
+ HasMedia bool
+ HasTeams bool
+ HasLabels bool
+ HasEstimates bool
+}
+
// APIProfile describes the shape of an API and what power-user features it warrants.
type APIProfile struct {
HighVolume bool
@@ -26,6 +53,8 @@ type APIProfile struct {
SyncableResources []string
SearchableFields map[string][]string
+
+ Domain DomainSignals
}
func Profile(s *spec.APISpec) *APIProfile {
@@ -153,6 +182,8 @@ func Profile(s *spec.APISpec) *APIProfile {
p.SearchableFields[resource] = sortedKeys(fields)
}
+ p.Domain = detectDomainSignals(s)
+
return p
}
@@ -173,6 +204,16 @@ func (p *APIProfile) ToVisionaryPlan(apiName string) *vision.VisionaryPlan {
},
}
+ plan.Domain = vision.DomainInfo{
+ Archetype: string(p.Domain.Archetype),
+ HasAssignees: p.Domain.HasAssignees,
+ HasDueDates: p.Domain.HasDueDates,
+ HasPriority: p.Domain.HasPriority,
+ HasTeams: p.Domain.HasTeams,
+ HasLabels: p.Domain.HasLabels,
+ HasEstimates: p.Domain.HasEstimates,
+ }
+
plan.Architecture = append(plan.Architecture,
vision.ArchitectureDecision{
Area: "persistence",
@@ -504,3 +545,110 @@ func sortedKeys[V any](m map[string]V) []string {
sort.Strings(keys)
return keys
}
+
+func detectDomainSignals(s *spec.APISpec) DomainSignals {
+ if s == nil {
+ return DomainSignals{Archetype: ArchetypeGeneric}
+ }
+
+ scores := map[DomainArchetype]int{
+ ArchetypeCommunication: 0,
+ ArchetypeProjectMgmt: 0,
+ ArchetypePayments: 0,
+ ArchetypeInfrastructure: 0,
+ ArchetypeContent: 0,
+ ArchetypeCRM: 0,
+ ArchetypeDeveloperPlatform: 0,
+ }
+
+ resourceKeywords := map[DomainArchetype][]string{
+ ArchetypeCommunication: {"message", "channel", "chat", "thread", "conversation", "dm", "reaction"},
+ ArchetypeProjectMgmt: {"issue", "task", "ticket", "project", "sprint", "milestone", "board", "epic", "backlog"},
+ ArchetypePayments: {"charge", "payment", "invoice", "subscription", "refund", "payout", "transaction", "balance", "transfer"},
+ ArchetypeInfrastructure: {"server", "instance", "cluster", "deployment", "container", "node", "pod", "volume", "network"},
+ ArchetypeContent: {"article", "post", "page", "blog", "content", "document", "media", "asset", "collection"},
+ ArchetypeCRM: {"contact", "deal", "lead", "opportunity", "account", "pipeline", "company", "person"},
+ ArchetypeDeveloperPlatform: {"repository", "commit", "branch", "pull_request", "merge_request", "pipeline", "build", "release", "package"},
+ }
+
+ ds := DomainSignals{}
+
+ var walkResources func(name string, r spec.Resource)
+ walkResources = func(name string, r spec.Resource) {
+ nameLower := strings.ToLower(name)
+ for archetype, keywords := range resourceKeywords {
+ for _, kw := range keywords {
+ if strings.Contains(nameLower, kw) {
+ scores[archetype] += 2
+ }
+ }
+ }
+
+ for _, endpoint := range r.Endpoints {
+ scanFieldSignals(endpoint.Params, &ds)
+ scanFieldSignals(endpoint.Body, &ds)
+ }
+
+ for subName, sub := range r.SubResources {
+ walkResources(subName, sub)
+ }
+ }
+
+ for name, resource := range s.Resources {
+ walkResources(name, resource)
+ }
+
+ // Pick the archetype with the highest score
+ bestArchetype := ArchetypeGeneric
+ bestScore := 0
+ for archetype, score := range scores {
+ if score > bestScore {
+ bestScore = score
+ bestArchetype = archetype
+ }
+ }
+ ds.Archetype = bestArchetype
+
+ return ds
+}
+
+func scanFieldSignals(params []spec.Param, ds *DomainSignals) {
+ for _, param := range params {
+ name := strings.ToLower(param.Name)
+
+ if strings.Contains(name, "assignee") || name == "assignee_id" || name == "assigned_to" {
+ ds.HasAssignees = true
+ }
+ if strings.Contains(name, "priority") {
+ ds.HasPriority = true
+ }
+ if strings.Contains(name, "due_date") || strings.Contains(name, "due_at") || strings.Contains(name, "deadline") {
+ ds.HasDueDates = true
+ }
+ if strings.Contains(name, "team") || name == "team_id" {
+ ds.HasTeams = true
+ }
+ if strings.Contains(name, "label") || strings.Contains(name, "tag") {
+ ds.HasLabels = true
+ }
+ if strings.Contains(name, "estimate") || strings.Contains(name, "story_points") || strings.Contains(name, "points") {
+ ds.HasEstimates = true
+ }
+ if strings.Contains(name, "thread") || strings.Contains(name, "reply_to") || strings.Contains(name, "parent_id") {
+ ds.HasThreading = true
+ }
+ if strings.Contains(name, "amount") || strings.Contains(name, "currency") || strings.Contains(name, "price") {
+ ds.HasTransactions = true
+ }
+ if strings.Contains(name, "subscription") || strings.Contains(name, "recurring") || strings.Contains(name, "interval") {
+ ds.HasSubscriptions = true
+ }
+ if strings.Contains(name, "media") || strings.Contains(name, "attachment") || strings.Contains(name, "image") || strings.Contains(name, "file") {
+ ds.HasMedia = true
+ }
+
+ if len(param.Fields) > 0 {
+ scanFieldSignals(param.Fields, ds)
+ }
+ }
+}
diff --git a/internal/vision/insight.go b/internal/vision/insight.go
new file mode 100644
index 00000000..a7a3f9cf
--- /dev/null
+++ b/internal/vision/insight.go
@@ -0,0 +1,19 @@
+package vision
+
+type NonObviousInsight struct {
+ ObviousFrame string
+ InsightFrame string
+ SignalSentence string
+ Implications []string
+}
+
+func (n NonObviousInsight) Formula(apiName string) string {
+ if n.InsightFrame == "" {
+ return ""
+ }
+ return apiName + " isn't just " + n.ObviousFrame + ". It's " + n.InsightFrame + ". " + n.SignalSentence
+}
+
+func (n NonObviousInsight) HasInsight() bool {
+ return n.InsightFrame != "" && len(n.Implications) > 0
+}
diff --git a/internal/vision/vision.go b/internal/vision/vision.go
index f4657f50..fea357d2 100644
--- a/internal/vision/vision.go
+++ b/internal/vision/vision.go
@@ -79,9 +79,21 @@ func (f *FeatureIdea) ComputeScore() int {
f.CompetitiveMoat
}
+type DomainInfo struct {
+ Archetype string
+ HasAssignees bool
+ HasDueDates bool
+ HasPriority bool
+ HasTeams bool
+ HasLabels bool
+ HasEstimates bool
+}
+
type VisionaryPlan struct {
APIName string
Identity APIIdentity
+ Insight NonObviousInsight
+ Domain DomainInfo
UsagePatterns []UsagePattern
ToolLandscape []ToolClassification
Workflows []Workflow
diff --git a/skills/printing-press/references/noi-examples.md b/skills/printing-press/references/noi-examples.md
new file mode 100644
index 00000000..8fb2297d
--- /dev/null
+++ b/skills/printing-press/references/noi-examples.md
@@ -0,0 +1,49 @@
+# Non-Obvious Insight (NOI) Formula
+
+## Formula
+
+`[API] isn't just [Obvious Frame]. It's [Insight Frame]. [Signal Sentence].`
+
+The NOI reframes what an API does from its surface-level description into a deeper
+insight about how power users actually leverage it. This drives CLI feature selection
+toward workflows that matter rather than CRUD wrappers.
+
+## Examples by Domain Archetype
+
+### Project Management
+
+1. **Linear**: Linear isn't just an issue tracker. It's a velocity sensor for engineering teams. Every sprint boundary reveals whether a team is accelerating or decelerating before managers notice.
+
+2. **Jira**: Jira isn't just a ticket system. It's an organizational memory of every decision and tradeoff. The comment threads on old tickets contain the rationale that onboarding docs never capture.
+
+3. **Asana**: Asana isn't just a task manager. It's a dependency graph that exposes bottlenecks. When three projects share the same blocked task, it reveals the single person everyone is waiting on.
+
+4. **GitHub Issues**: GitHub Issues isn't just a bug tracker. It's a public roadmap signal. The ratio of feature requests to bug reports tells you whether users want more or want better.
+
+### Communication
+
+5. **Slack**: Slack isn't just a messaging app. It's an attention allocator. The channels with the highest unread counts reveal the topics your organization cares about but hasn't staffed.
+
+6. **Discord**: Discord isn't just a chat platform. It's a community health monitor. Thread reply depth and reaction diversity predict whether a community is growing or hollowing out.
+
+7. **Twilio**: Twilio isn't just an SMS API. It's a delivery reliability surface. The gap between sent and delivered timestamps exposes carrier routing problems before customers complain.
+
+### Payments
+
+8. **Stripe**: Stripe isn't just a payment processor. It's a revenue pattern detector. Subscription churn clusters around specific billing cycle days, revealing pricing sensitivity windows.
+
+9. **Square**: Square isn't just a POS system. It's a foot traffic sensor. Transaction timestamps and amounts map customer behavior patterns that no survey captures.
+
+10. **Plaid**: Plaid isn't just a banking connector. It's a financial behavior classifier. Transaction categorization patterns reveal spending personalities that credit scores miss.
+
+### Infrastructure
+
+11. **AWS EC2**: EC2 isn't just a compute service. It's a capacity planning oracle. Instance type migration patterns across an org reveal which workloads are growing before the bills spike.
+
+12. **Cloudflare**: Cloudflare isn't just a CDN. It's a traffic intent classifier. The ratio of cached to uncached requests reveals whether your users are browsing or building.
+
+### Content
+
+13. **Notion**: Notion isn't just a wiki. It's a knowledge decay detector. Pages with high view counts but stale edit dates are the documentation traps that cause the most onboarding friction.
+
+14. **WordPress**: WordPress isn't just a CMS. It's a content performance lab. The correlation between publish timing, word count, and engagement reveals the format your audience actually wants.
← 31a8bed4 docs: comprehensive README rewrite for 8-phase pipeline
·
back to Cli Printing Press
·
feat(generator): add PM workflow and behavioral insight temp f55405b8 →