@geraldmaron/construct 1.0.4 → 1.0.5

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.
Files changed (90) hide show
  1. package/README.md +51 -42
  2. package/bin/construct +256 -4
  3. package/commands/understand/research.md +5 -3
  4. package/lib/auto-docs.mjs +2 -2
  5. package/lib/cli-commands.mjs +44 -0
  6. package/lib/comment-lint.mjs +7 -1
  7. package/lib/config/schema.mjs +3 -0
  8. package/lib/flavors/loader.mjs +136 -0
  9. package/lib/hooks/agent-tracker.mjs +22 -3
  10. package/lib/hooks/pre-push-gate.mjs +14 -1
  11. package/lib/hooks/session-optimize.mjs +3 -2
  12. package/lib/hooks/session-reflect.mjs +68 -0
  13. package/lib/init-unified.mjs +25 -2
  14. package/lib/intake/classify.mjs +61 -183
  15. package/lib/intake/prepare.mjs +7 -0
  16. package/lib/intake/tables/creative.mjs +94 -0
  17. package/lib/intake/tables/operations.mjs +85 -0
  18. package/lib/intake/tables/research.mjs +85 -0
  19. package/lib/intake/tables/rnd.mjs +175 -0
  20. package/lib/knowledge/research-store.mjs +109 -0
  21. package/lib/observation-store.mjs +19 -0
  22. package/lib/outcomes/aggregate.mjs +104 -0
  23. package/lib/outcomes/record.mjs +115 -0
  24. package/lib/parity.mjs +6 -9
  25. package/lib/profiles/lifecycle.mjs +388 -0
  26. package/lib/profiles/loader.mjs +123 -0
  27. package/lib/profiles/validate-custom.mjs +114 -0
  28. package/lib/reflect/extractor.mjs +193 -0
  29. package/lib/reflect.mjs +89 -2
  30. package/lib/sandbox.mjs +102 -0
  31. package/package.json +6 -1
  32. package/personas/construct.md +20 -20
  33. package/platforms/claude/settings.template.json +13 -0
  34. package/scripts/sync-agents.mjs +11 -0
  35. package/skills/roles/architect.ai-systems.md +4 -2
  36. package/skills/roles/architect.data.md +4 -2
  37. package/skills/roles/architect.enterprise.md +4 -2
  38. package/skills/roles/architect.integration.md +4 -2
  39. package/skills/roles/architect.md +7 -5
  40. package/skills/roles/architect.platform.md +4 -2
  41. package/skills/roles/data-analyst.experiment.md +4 -2
  42. package/skills/roles/data-analyst.md +9 -7
  43. package/skills/roles/data-analyst.product-intelligence.md +4 -2
  44. package/skills/roles/data-analyst.product.md +4 -2
  45. package/skills/roles/data-analyst.telemetry.md +4 -2
  46. package/skills/roles/data-engineer.pipeline.md +4 -2
  47. package/skills/roles/data-engineer.vector-retrieval.md +4 -2
  48. package/skills/roles/data-engineer.warehouse.md +4 -2
  49. package/skills/roles/debugger.md +7 -5
  50. package/skills/roles/designer.accessibility.md +4 -2
  51. package/skills/roles/designer.md +10 -8
  52. package/skills/roles/engineer.ai.md +4 -2
  53. package/skills/roles/engineer.data.md +5 -3
  54. package/skills/roles/engineer.md +14 -12
  55. package/skills/roles/engineer.platform.md +5 -3
  56. package/skills/roles/operator.docs.md +6 -4
  57. package/skills/roles/operator.md +6 -4
  58. package/skills/roles/operator.release.md +4 -2
  59. package/skills/roles/operator.sre.md +5 -3
  60. package/skills/roles/orchestrator.md +5 -3
  61. package/skills/roles/product-manager.ai-product.md +4 -2
  62. package/skills/roles/product-manager.business-strategy.md +4 -2
  63. package/skills/roles/product-manager.enterprise.md +4 -2
  64. package/skills/roles/product-manager.growth.md +4 -2
  65. package/skills/roles/product-manager.md +6 -4
  66. package/skills/roles/product-manager.platform.md +4 -2
  67. package/skills/roles/product-manager.product.md +4 -2
  68. package/skills/roles/qa.ai-eval.md +4 -2
  69. package/skills/roles/qa.api-contract.md +4 -2
  70. package/skills/roles/qa.data-pipeline.md +4 -2
  71. package/skills/roles/qa.md +7 -5
  72. package/skills/roles/qa.test-automation.md +5 -3
  73. package/skills/roles/qa.web-ui.md +4 -2
  74. package/skills/roles/researcher.explorer.md +4 -2
  75. package/skills/roles/researcher.md +11 -9
  76. package/skills/roles/researcher.ux.md +4 -2
  77. package/skills/roles/reviewer.devil-advocate.md +4 -2
  78. package/skills/roles/reviewer.evaluator.md +4 -2
  79. package/skills/roles/reviewer.md +14 -12
  80. package/skills/roles/reviewer.trace.md +4 -2
  81. package/skills/roles/security.ai.md +4 -2
  82. package/skills/roles/security.appsec.md +4 -2
  83. package/skills/roles/security.cloud.md +4 -2
  84. package/skills/roles/security.legal-compliance.md +4 -2
  85. package/skills/roles/security.md +7 -5
  86. package/skills/roles/security.privacy.md +4 -2
  87. package/skills/roles/security.supply-chain.md +4 -2
  88. package/templates/docs/persona-artifact.md +36 -0
  89. package/templates/docs/research-finding.md +26 -0
  90. package/templates/docs/skill-artifact.md +27 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * lib/intake/tables/creative.mjs — Creative production intake classification.
3
+ *
4
+ * Loop: brief → research → draft → review → publish → measure. Covers content
5
+ * creation, campaigns, design work. Roles are creative-org roles, not engineering.
6
+ */
7
+
8
+ export const INTAKE_TYPES = ['brief', 'content-request', 'asset', 'experiment', 'report', 'legal-compliance', 'ops', 'unknown'];
9
+ export const STAGES = ['brief', 'research', 'draft', 'review', 'publish', 'measure'];
10
+
11
+ export const UNKNOWN_TRIAGE = {
12
+ intakeType: 'unknown',
13
+ rdStage: 'unknown',
14
+ primaryOwner: 'operator',
15
+ recommendedChain: ['operator'],
16
+ recommendedAction: 'summarize',
17
+ risk: 'low',
18
+ requiresApproval: false,
19
+ };
20
+
21
+ export const CLASSIFICATION_TABLE = [
22
+ {
23
+ intakeType: 'legal-compliance',
24
+ keywords: ['gdpr', 'ccpa', 'privacy', 'consent', 'unsubscribe', 'can-spam', 'cookie banner', 'disclosure', 'trademark'],
25
+ rdStage: 'review',
26
+ primaryOwner: 'legal-compliance',
27
+ recommendedChain: ['legal-compliance', 'reviewer'],
28
+ recommendedAction: 'clarify',
29
+ risk: 'high',
30
+ requiresApproval: true,
31
+ },
32
+ {
33
+ intakeType: 'brief',
34
+ keywords: ['brief', 'campaign', 'launch plan', 'go-to-market', 'gtm', 'positioning', 'narrative', 'audience'],
35
+ rdStage: 'brief',
36
+ primaryOwner: 'product-lead',
37
+ recommendedChain: ['product-lead', 'researcher', 'content-writer'],
38
+ recommendedAction: 'draft-prd',
39
+ risk: 'medium',
40
+ requiresApproval: false,
41
+ },
42
+ {
43
+ intakeType: 'content-request',
44
+ keywords: ['blog', 'article', 'landing page', 'social post', 'newsletter', 'email copy', 'video script', 'headline', 'post', 'content'],
45
+ rdStage: 'draft',
46
+ primaryOwner: 'content-writer',
47
+ recommendedChain: ['content-writer', 'reviewer'],
48
+ recommendedAction: 'draft-rfc',
49
+ risk: 'low',
50
+ requiresApproval: false,
51
+ },
52
+ {
53
+ intakeType: 'asset',
54
+ keywords: ['asset', 'logo', 'image', 'banner', 'mockup', 'figma', 'brand guide', 'illustration'],
55
+ rdStage: 'draft',
56
+ primaryOwner: 'designer',
57
+ recommendedChain: ['designer', 'reviewer'],
58
+ recommendedAction: 'draft-rfc',
59
+ risk: 'low',
60
+ requiresApproval: false,
61
+ },
62
+ {
63
+ intakeType: 'experiment',
64
+ keywords: ['a/b test', 'experiment', 'hypothesis', 'multivariate', 'pilot', 'audience test'],
65
+ rdStage: 'measure',
66
+ primaryOwner: 'data-analyst',
67
+ recommendedChain: ['data-analyst', 'researcher'],
68
+ recommendedAction: 'create-experiment',
69
+ risk: 'low',
70
+ requiresApproval: false,
71
+ },
72
+ {
73
+ intakeType: 'report',
74
+ keywords: ['report', 'dashboard', 'attribution', 'ctr', 'cpa', 'roas', 'conversion', 'funnel', 'engagement'],
75
+ rdStage: 'measure',
76
+ primaryOwner: 'data-analyst',
77
+ recommendedChain: ['data-analyst', 'product-lead'],
78
+ recommendedAction: 'evaluate',
79
+ risk: 'low',
80
+ requiresApproval: false,
81
+ },
82
+ {
83
+ intakeType: 'ops',
84
+ keywords: ['process', 'workflow', 'template', 'sla', 'calendar', 'planning', 'editorial calendar'],
85
+ rdStage: 'review',
86
+ primaryOwner: 'operator',
87
+ recommendedChain: ['operator', 'product-lead'],
88
+ recommendedAction: 'create-runbook',
89
+ risk: 'low',
90
+ requiresApproval: false,
91
+ },
92
+ ];
93
+
94
+ export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * lib/intake/tables/operations.mjs — Operations intake classification.
3
+ *
4
+ * Loop: request → triage → resolve → document → improve. Optimized for teams
5
+ * whose primary work is fulfilling requests and keeping things running, not
6
+ * building new product. Owners are operations roles (operator, sre, engineer).
7
+ */
8
+
9
+ export const INTAKE_TYPES = ['request', 'bug', 'incident', 'ops', 'security', 'docs', 'unknown'];
10
+ export const STAGES = ['request', 'triage', 'resolve', 'document', 'improve'];
11
+
12
+ export const UNKNOWN_TRIAGE = {
13
+ intakeType: 'unknown',
14
+ rdStage: 'unknown',
15
+ primaryOwner: 'operator',
16
+ recommendedChain: ['operator'],
17
+ recommendedAction: 'summarize',
18
+ risk: 'low',
19
+ requiresApproval: false,
20
+ };
21
+
22
+ export const CLASSIFICATION_TABLE = [
23
+ {
24
+ intakeType: 'security',
25
+ keywords: ['security', 'cve', 'vulnerability', 'leak', 'auth bypass', 'pii', 'secret'],
26
+ rdStage: 'resolve',
27
+ primaryOwner: 'security',
28
+ recommendedChain: ['security', 'engineer', 'reviewer'],
29
+ recommendedAction: 'diagnose',
30
+ risk: 'high',
31
+ requiresApproval: true,
32
+ },
33
+ {
34
+ intakeType: 'incident',
35
+ keywords: ['incident', 'outage', 'down', 'p0 ', 'p1 ', 'oncall', 'pager', '5xx', 'sla breach'],
36
+ rdStage: 'resolve',
37
+ primaryOwner: 'sre',
38
+ recommendedChain: ['sre', 'engineer'],
39
+ recommendedAction: 'create-runbook',
40
+ risk: 'high',
41
+ requiresApproval: true,
42
+ },
43
+ {
44
+ intakeType: 'bug',
45
+ keywords: ['bug', 'broken', 'error', 'fails', 'crash', 'exception', 'stack trace'],
46
+ rdStage: 'resolve',
47
+ primaryOwner: 'engineer',
48
+ recommendedChain: ['engineer', 'qa', 'reviewer'],
49
+ recommendedAction: 'diagnose',
50
+ risk: 'medium',
51
+ requiresApproval: false,
52
+ },
53
+ {
54
+ intakeType: 'docs',
55
+ keywords: ['docs', 'documentation', 'how to', 'guide', 'readme', 'wiki', 'runbook missing'],
56
+ rdStage: 'document',
57
+ primaryOwner: 'docs-keeper',
58
+ recommendedChain: ['docs-keeper', 'reviewer'],
59
+ recommendedAction: 'create-runbook',
60
+ risk: 'low',
61
+ requiresApproval: false,
62
+ },
63
+ {
64
+ intakeType: 'ops',
65
+ keywords: ['cron', 'scheduled job', 'maintenance', 'backup', 'restore', 'rotation', 'capacity', 'upgrade'],
66
+ rdStage: 'improve',
67
+ primaryOwner: 'sre',
68
+ recommendedChain: ['sre', 'engineer'],
69
+ recommendedAction: 'create-runbook',
70
+ risk: 'low',
71
+ requiresApproval: false,
72
+ },
73
+ {
74
+ intakeType: 'request',
75
+ keywords: ['request', 'access', 'feature request', 'integration', 'new tool', 'change request', 'how do i', 'can someone'],
76
+ rdStage: 'triage',
77
+ primaryOwner: 'operator',
78
+ recommendedChain: ['operator', 'engineer'],
79
+ recommendedAction: 'clarify',
80
+ risk: 'low',
81
+ requiresApproval: false,
82
+ },
83
+ ];
84
+
85
+ export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * lib/intake/tables/research.mjs — Research intake classification.
3
+ *
4
+ * Loop: question → gather → analyze → synthesize → recommend. Covers analysis,
5
+ * market intelligence, evaluation, advisory work. Output is a written
6
+ * recommendation, not a shipped product.
7
+ */
8
+
9
+ export const INTAKE_TYPES = ['question', 'study', 'synthesis', 'report', 'experiment', 'ops', 'unknown'];
10
+ export const STAGES = ['question', 'gather', 'analyze', 'synthesize', 'recommend'];
11
+
12
+ export const UNKNOWN_TRIAGE = {
13
+ intakeType: 'unknown',
14
+ rdStage: 'unknown',
15
+ primaryOwner: 'researcher',
16
+ recommendedChain: ['researcher'],
17
+ recommendedAction: 'summarize',
18
+ risk: 'low',
19
+ requiresApproval: false,
20
+ };
21
+
22
+ export const CLASSIFICATION_TABLE = [
23
+ {
24
+ intakeType: 'study',
25
+ keywords: ['interview', 'user study', 'survey', 'focus group', 'usability test', 'diary study', 'ethnography'],
26
+ rdStage: 'gather',
27
+ primaryOwner: 'ux-researcher',
28
+ recommendedChain: ['ux-researcher', 'researcher'],
29
+ recommendedAction: 'create-experiment',
30
+ risk: 'low',
31
+ requiresApproval: false,
32
+ },
33
+ {
34
+ intakeType: 'experiment',
35
+ keywords: ['hypothesis', 'experiment', 'controlled trial', 'a/b test', 'pilot', 'falsifiable'],
36
+ rdStage: 'analyze',
37
+ primaryOwner: 'evaluator',
38
+ recommendedChain: ['evaluator', 'researcher', 'data-analyst'],
39
+ recommendedAction: 'evaluate',
40
+ risk: 'low',
41
+ requiresApproval: false,
42
+ },
43
+ {
44
+ intakeType: 'report',
45
+ keywords: ['report', 'finding', 'dashboard', 'metric', 'benchmark', 'baseline', 'comparison'],
46
+ rdStage: 'recommend',
47
+ primaryOwner: 'data-analyst',
48
+ recommendedChain: ['data-analyst', 'researcher', 'reviewer'],
49
+ recommendedAction: 'evaluate',
50
+ risk: 'low',
51
+ requiresApproval: false,
52
+ },
53
+ {
54
+ intakeType: 'synthesis',
55
+ keywords: ['synthesis', 'literature review', 'meta', 'state of the art', 'roundup', 'survey paper', 'taxonomy'],
56
+ rdStage: 'synthesize',
57
+ primaryOwner: 'researcher',
58
+ recommendedChain: ['researcher', 'reviewer'],
59
+ recommendedAction: 'research',
60
+ risk: 'low',
61
+ requiresApproval: false,
62
+ },
63
+ {
64
+ intakeType: 'question',
65
+ keywords: ['question', 'research question', 'what is', 'why does', 'how do', 'investigate', 'figure out', 'understand'],
66
+ rdStage: 'question',
67
+ primaryOwner: 'researcher',
68
+ recommendedChain: ['researcher'],
69
+ recommendedAction: 'clarify',
70
+ risk: 'low',
71
+ requiresApproval: false,
72
+ },
73
+ {
74
+ intakeType: 'ops',
75
+ keywords: ['process', 'methodology', 'template', 'protocol', 'consent form', 'data handling'],
76
+ rdStage: 'gather',
77
+ primaryOwner: 'operator',
78
+ recommendedChain: ['operator', 'researcher'],
79
+ recommendedAction: 'create-runbook',
80
+ risk: 'low',
81
+ requiresApproval: false,
82
+ },
83
+ ];
84
+
85
+ export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
@@ -0,0 +1,175 @@
1
+ /**
2
+ * lib/intake/tables/rnd.mjs — RND intake classification table.
3
+ *
4
+ * Extracted from lib/intake/classify.mjs as part of B2. Behavior must remain
5
+ * byte-identical for RND inputs; tests/intake/golden-rnd.test.mjs is the
6
+ * regression guard.
7
+ *
8
+ * Schema: { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE }.
9
+ * UNKNOWN_TRIAGE handles the no-match fallback for this profile.
10
+ */
11
+
12
+ export const INTAKE_TYPES = [
13
+ 'user-signal',
14
+ 'bug',
15
+ 'requirement',
16
+ 'research',
17
+ 'experiment',
18
+ 'eval-finding',
19
+ 'architecture',
20
+ 'incident',
21
+ 'launch-asset',
22
+ 'ops',
23
+ 'security',
24
+ 'legal-compliance',
25
+ 'unknown',
26
+ ];
27
+
28
+ export const STAGES = [
29
+ 'signal',
30
+ 'framing',
31
+ 'hypothesis',
32
+ 'research',
33
+ 'artifact',
34
+ 'design',
35
+ 'implementation',
36
+ 'evaluation',
37
+ 'release',
38
+ 'operations',
39
+ 'unknown',
40
+ ];
41
+
42
+ export const UNKNOWN_TRIAGE = {
43
+ intakeType: 'unknown',
44
+ rdStage: 'unknown',
45
+ primaryOwner: 'orchestrator',
46
+ recommendedChain: ['orchestrator'],
47
+ recommendedAction: 'summarize',
48
+ risk: 'low',
49
+ requiresApproval: false,
50
+ };
51
+
52
+ export const CLASSIFICATION_TABLE = [
53
+ {
54
+ intakeType: 'security',
55
+ keywords: ['security', 'secret', 'cve', 'vulnerability', 'vuln', 'exploit', 'leak', 'auth bypass', 'privilege escalation', 'sqli', 'xss', 'csrf', 'rce'],
56
+ rdStage: 'operations',
57
+ primaryOwner: 'security',
58
+ recommendedChain: ['security', 'engineer', 'reviewer'],
59
+ recommendedAction: 'diagnose',
60
+ risk: 'high',
61
+ requiresApproval: true,
62
+ },
63
+ {
64
+ intakeType: 'incident',
65
+ keywords: ['incident', 'outage', 'slo breach', 'sla breach', 'latency spike', 'availability', 'down', 'p0 ', 'p1 ', 'pagerduty', '5xx', 'oncall'],
66
+ rdStage: 'operations',
67
+ primaryOwner: 'sre',
68
+ recommendedChain: ['sre', 'debugger', 'platform-engineer'],
69
+ recommendedAction: 'create-runbook',
70
+ risk: 'high',
71
+ requiresApproval: true,
72
+ },
73
+ {
74
+ intakeType: 'legal-compliance',
75
+ keywords: ['gdpr', 'ccpa', 'hipaa', 'sox', 'soc2', 'license', 'lawsuit', 'dpa', 'data retention', 'pii', 'subpoena', 'compliance audit'],
76
+ rdStage: 'operations',
77
+ primaryOwner: 'legal-compliance',
78
+ recommendedChain: ['legal-compliance', 'security', 'product-manager'],
79
+ recommendedAction: 'clarify',
80
+ risk: 'high',
81
+ requiresApproval: true,
82
+ },
83
+ {
84
+ intakeType: 'architecture',
85
+ keywords: ['architecture', 'adr', 'rfc', 'interface', 'tradeoff', 'boundary', 'system design', 'data model', 'api contract', 'migration plan'],
86
+ rdStage: 'design',
87
+ primaryOwner: 'architect',
88
+ recommendedChain: ['architect', 'devil-advocate', 'engineer'],
89
+ recommendedAction: 'draft-rfc',
90
+ risk: 'medium',
91
+ requiresApproval: false,
92
+ },
93
+ {
94
+ intakeType: 'eval-finding',
95
+ keywords: ['eval', 'evaluation', 'hallucination', 'judge', 'trace', 'score regression', 'recall@', 'precision@', 'mrr', 'ndcg', 'failure case', 'rubric'],
96
+ rdStage: 'evaluation',
97
+ primaryOwner: 'evaluator',
98
+ recommendedChain: ['evaluator', 'ai-engineer', 'trace-reviewer'],
99
+ recommendedAction: 'evaluate',
100
+ risk: 'medium',
101
+ requiresApproval: false,
102
+ },
103
+ {
104
+ intakeType: 'bug',
105
+ keywords: ['bug', 'broken', 'error', 'stack trace', 'regression', 'crash', 'exception', 'fails', 'failing', 'throws', 'not working', 'reproduce', 'repro:'],
106
+ rdStage: 'implementation',
107
+ primaryOwner: 'debugger',
108
+ recommendedChain: ['debugger', 'engineer', 'qa', 'reviewer'],
109
+ recommendedAction: 'diagnose',
110
+ risk: 'medium',
111
+ requiresApproval: false,
112
+ },
113
+ {
114
+ intakeType: 'experiment',
115
+ keywords: ['hypothesis', 'experiment', 'spike', 'prototype', 'falsifiable', 'research question', 'a/b test', 'pilot'],
116
+ rdStage: 'hypothesis',
117
+ primaryOwner: 'rd-lead',
118
+ recommendedChain: ['rd-lead', 'researcher', 'evaluator'],
119
+ recommendedAction: 'create-experiment',
120
+ risk: 'low',
121
+ requiresApproval: false,
122
+ },
123
+ {
124
+ intakeType: 'launch-asset',
125
+ keywords: ['release', 'changelog', 'version bump', 'ship', 'launch', 'rollout', 'cut a release', 'rc1', 'rc2', 'release candidate'],
126
+ rdStage: 'release',
127
+ primaryOwner: 'release-manager',
128
+ recommendedChain: ['release-manager', 'qa', 'docs-keeper'],
129
+ recommendedAction: 'release-review',
130
+ risk: 'medium',
131
+ requiresApproval: false,
132
+ },
133
+ {
134
+ intakeType: 'research',
135
+ keywords: ['competitor', 'market', 'pricing', 'positioning', 'industry', 'state of the art', 'literature', 'benchmark study', 'desk research'],
136
+ rdStage: 'research',
137
+ primaryOwner: 'business-strategist',
138
+ recommendedChain: ['business-strategist', 'researcher', 'product-manager'],
139
+ recommendedAction: 'research',
140
+ risk: 'low',
141
+ requiresApproval: false,
142
+ },
143
+ {
144
+ intakeType: 'user-signal',
145
+ keywords: ['customer', 'feedback', 'pain point', 'user says', 'user feedback', 'support ticket', 'churn', 'nps', 'usability', 'frustrated'],
146
+ rdStage: 'signal',
147
+ primaryOwner: 'product-manager',
148
+ recommendedChain: ['product-manager', 'ux-researcher', 'researcher'],
149
+ recommendedAction: 'clarify',
150
+ risk: 'low',
151
+ requiresApproval: false,
152
+ },
153
+ {
154
+ intakeType: 'requirement',
155
+ keywords: ['acceptance criteria', 'requirement', 'must have', 'should have', 'feature request', 'prd', 'use case', 'success metric'],
156
+ rdStage: 'framing',
157
+ primaryOwner: 'product-manager',
158
+ recommendedChain: ['product-manager', 'architect', 'engineer'],
159
+ recommendedAction: 'draft-prd',
160
+ risk: 'low',
161
+ requiresApproval: false,
162
+ },
163
+ {
164
+ intakeType: 'ops',
165
+ keywords: ['runbook', 'cron', 'scheduled job', 'maintenance', 'backup', 'restore', 'capacity plan', 'cost optimization', 'dependency upgrade'],
166
+ rdStage: 'operations',
167
+ primaryOwner: 'operations',
168
+ recommendedChain: ['operations', 'sre', 'engineer'],
169
+ recommendedAction: 'create-runbook',
170
+ risk: 'low',
171
+ requiresApproval: false,
172
+ },
173
+ ];
174
+
175
+ export default { INTAKE_TYPES, STAGES, CLASSIFICATION_TABLE, UNKNOWN_TRIAGE };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * lib/knowledge/research-store.mjs — Persist research findings into the knowledge base.
3
+ *
4
+ * Wraps the existing document-ingest pipeline so research output written via
5
+ * `construct knowledge add --source=research` lands in
6
+ * `.cx/knowledge/external/research/<slug>.md` with research-specific frontmatter
7
+ * (topic, confidence, sources, expiresAt, profile). The file is then synced into
8
+ * the SQL/vector index via the standard `syncFileStateToSql` path.
9
+ *
10
+ * Schema (frontmatter):
11
+ * kind: research-finding
12
+ * topic: string
13
+ * confidence: confirmed | inferred | weak
14
+ * sources: [{ url, accessedAt, span? }]
15
+ * expiresAt: ISO date (default +90d)
16
+ * profile: <profile-id>
17
+ */
18
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { syncFileStateToSql } from '../storage/sync.mjs';
21
+ import { resolveActiveProfile } from '../profiles/loader.mjs';
22
+
23
+ const ROOT = '.cx/knowledge/external/research';
24
+ const MAX_BYTES = 50 * 1024;
25
+ const DEFAULT_TTL_DAYS = 90;
26
+
27
+ const VALID_CONFIDENCE = new Set(['confirmed', 'inferred', 'weak']);
28
+
29
+ /**
30
+ * @param {object} args
31
+ * @param {string} args.cwd - project root
32
+ * @param {string} args.slug - filename slug (lowercase, hyphenated)
33
+ * @param {string} args.topic - human-readable topic line
34
+ * @param {string} args.body - the FINDINGS / INFERENCES / GAPS / RECOMMENDATION block
35
+ * @param {string} [args.confidence] - confirmed | inferred | weak
36
+ * @param {Array<{url:string,accessedAt?:string,span?:string}>} [args.sources]
37
+ * @param {number} [args.ttlDays] - override default expiry
38
+ * @returns {Promise<{path:string,bytes:number}>}
39
+ */
40
+ export async function addResearchFinding({
41
+ cwd,
42
+ slug,
43
+ topic,
44
+ body,
45
+ confidence = 'inferred',
46
+ sources = [],
47
+ ttlDays = DEFAULT_TTL_DAYS,
48
+ }) {
49
+ if (!cwd) throw new Error('addResearchFinding: cwd is required');
50
+ if (!slug || !/^[a-z0-9][a-z0-9-]{0,60}$/.test(slug)) {
51
+ throw new Error('addResearchFinding: slug must be lowercase, hyphenated, max 60 chars');
52
+ }
53
+ if (!topic || typeof topic !== 'string') {
54
+ throw new Error('addResearchFinding: topic is required');
55
+ }
56
+ if (!body || typeof body !== 'string') {
57
+ throw new Error('addResearchFinding: body is required');
58
+ }
59
+ if (!VALID_CONFIDENCE.has(confidence)) {
60
+ throw new Error(`addResearchFinding: confidence must be one of ${Array.from(VALID_CONFIDENCE).join(', ')}`);
61
+ }
62
+ if (confidence === 'confirmed' && (!Array.isArray(sources) || sources.length === 0)) {
63
+ throw new Error('addResearchFinding: confidence=confirmed requires at least one source');
64
+ }
65
+
66
+ const profile = resolveActiveProfile(cwd);
67
+ const now = new Date();
68
+ const expiresAt = new Date(now.getTime() + ttlDays * 24 * 60 * 60 * 1000).toISOString();
69
+
70
+ const outDir = join(cwd, ROOT);
71
+ mkdirSync(outDir, { recursive: true });
72
+ const outPath = join(outDir, `${slug}.md`);
73
+
74
+ const fmSources = (Array.isArray(sources) ? sources : [])
75
+ .filter((s) => s && typeof s.url === 'string')
76
+ .map((s) => ({
77
+ url: s.url,
78
+ accessedAt: s.accessedAt || now.toISOString(),
79
+ ...(s.span ? { span: String(s.span).slice(0, 200) } : {}),
80
+ }));
81
+
82
+ const frontmatter = [
83
+ '---',
84
+ 'kind: research-finding',
85
+ `topic: ${JSON.stringify(topic)}`,
86
+ `confidence: ${confidence}`,
87
+ `sources: ${JSON.stringify(fmSources)}`,
88
+ `created: ${now.toISOString()}`,
89
+ `expiresAt: ${expiresAt}`,
90
+ `profile: ${profile?.id ?? 'rnd'}`,
91
+ '---',
92
+ '',
93
+ ].join('\n');
94
+
95
+ const fullText = frontmatter + body.trim() + '\n';
96
+ if (Buffer.byteLength(fullText, 'utf8') > MAX_BYTES) {
97
+ throw new Error(`addResearchFinding: file exceeds ${MAX_BYTES} bytes`);
98
+ }
99
+
100
+ writeFileSync(outPath, fullText);
101
+
102
+ // Best-effort sync into SQL/vector index. The file is the source of truth;
103
+ // index lag never blocks the operator.
104
+ try {
105
+ await syncFileStateToSql(cwd, { project: profile?.id ?? 'rnd' });
106
+ } catch { /* best effort */ }
107
+
108
+ return { path: outPath, bytes: Buffer.byteLength(fullText, 'utf8') };
109
+ }
@@ -58,6 +58,20 @@ function generateId() {
58
58
  return `obs-${ts}-${rand}`;
59
59
  }
60
60
 
61
+ const MAX_EXTRAS_BYTES = 2 * 1024;
62
+
63
+ function sanitizeExtras(extras) {
64
+ if (extras == null) return null;
65
+ if (typeof extras !== 'object' || Array.isArray(extras)) return null;
66
+ try {
67
+ const serialized = JSON.stringify(extras);
68
+ if (Buffer.byteLength(serialized, 'utf8') > MAX_EXTRAS_BYTES) return null;
69
+ return JSON.parse(serialized);
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
61
75
  function clamp(str, max) {
62
76
  if (!str || str.length <= max) return str || null;
63
77
  return str.slice(0, max - 1) + '\u2026';
@@ -131,6 +145,7 @@ export async function addObservation(rootDir, {
131
145
  confidence = 0.8,
132
146
  source = null,
133
147
  gitSha = null,
148
+ extras = null,
134
149
  } = {}) {
135
150
  const id = generateId();
136
151
  const now = new Date().toISOString();
@@ -139,6 +154,9 @@ export async function addObservation(rootDir, {
139
154
  const clampedSummary = clamp(String(summary), MAX_SUMMARY);
140
155
  const clampedContent = clamp(String(content), MAX_CONTENT);
141
156
  const clampedTags = (Array.isArray(tags) ? tags : []).slice(0, MAX_TAGS).map(String);
157
+ // Structured metadata: opt-in. Capped at 2 KB stringified so a misuse can't
158
+ // bloat the per-id JSON. Plain object only; rejects arrays and functions.
159
+ const clampedExtras = sanitizeExtras(extras);
142
160
 
143
161
  const record = {
144
162
  id,
@@ -151,6 +169,7 @@ export async function addObservation(rootDir, {
151
169
  confidence: Math.max(0, Math.min(1, Number(confidence) || 0.8)),
152
170
  source: source || null,
153
171
  gitSha: gitSha ? String(gitSha).slice(0, 40) : null,
172
+ extras: clampedExtras,
154
173
  createdAt: now,
155
174
  updatedAt: now,
156
175
  };