@geraldmaron/construct 1.0.3 → 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.
- package/README.md +51 -42
- package/agents/prompts/cx-ai-engineer.md +6 -26
- package/agents/prompts/cx-architect.md +1 -0
- package/agents/prompts/cx-business-strategist.md +2 -0
- package/agents/prompts/cx-data-analyst.md +6 -26
- package/agents/prompts/cx-docs-keeper.md +1 -31
- package/agents/prompts/cx-explorer.md +1 -0
- package/agents/prompts/cx-orchestrator.md +40 -112
- package/agents/prompts/cx-platform-engineer.md +2 -22
- package/agents/prompts/cx-product-manager.md +2 -1
- package/agents/prompts/cx-qa.md +0 -20
- package/agents/prompts/cx-rd-lead.md +2 -0
- package/agents/prompts/cx-researcher.md +77 -31
- package/agents/prompts/cx-security.md +11 -49
- package/agents/prompts/cx-sre.md +9 -43
- package/agents/prompts/cx-ux-researcher.md +1 -0
- package/agents/role-manifests.json +4 -4
- package/bin/construct +279 -4
- package/commands/understand/research.md +5 -3
- package/db/schema/004_recommendations.sql +46 -0
- package/db/schema/005_strategy.sql +21 -0
- package/lib/auto-docs.mjs +3 -4
- package/lib/beads-automation.mjs +16 -7
- package/lib/cli-commands.mjs +52 -2
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- package/lib/embed/conflict-detection.mjs +26 -9
- package/lib/embed/customer-profiles.mjs +37 -17
- package/lib/embed/daemon.mjs +10 -8
- package/lib/embed/recommendation-store.mjs +213 -14
- package/lib/embed/workspaces.mjs +53 -18
- package/lib/flavors/loader.mjs +136 -0
- package/lib/gates-audit.mjs +3 -3
- package/lib/health-check.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-compact.mjs +3 -0
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/read-tracker.mjs +10 -101
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/host-capabilities.mjs +90 -1
- package/lib/init-unified.mjs +25 -2
- package/lib/init-update.mjs +246 -131
- package/lib/intake/classify.mjs +61 -183
- package/lib/intake/prepare.mjs +7 -0
- package/lib/intake/tables/creative.mjs +94 -0
- package/lib/intake/tables/operations.mjs +85 -0
- package/lib/intake/tables/research.mjs +85 -0
- package/lib/intake/tables/rnd.mjs +175 -0
- package/lib/intent-classifier.mjs +1 -0
- package/lib/knowledge/layout.mjs +10 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/mcp/tools/telemetry.mjs +30 -78
- package/lib/model-router.mjs +61 -1
- package/lib/observation-store.mjs +19 -0
- package/lib/ollama-manager.mjs +1 -1
- package/lib/opencode-telemetry.mjs +4 -5
- package/lib/orchestration-policy.mjs +9 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +121 -21
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/prompt-composer.js +106 -29
- package/lib/read-tracker-store.mjs +149 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -0
- package/lib/server/index.mjs +76 -0
- package/lib/server/telemetry-login.mjs +17 -25
- package/lib/service-manager.mjs +30 -22
- package/lib/services/local-postgres.mjs +15 -0
- package/lib/services/telemetry-backend.mjs +1 -2
- package/lib/setup.mjs +8 -43
- package/lib/status.mjs +51 -5
- package/lib/storage/backend.mjs +12 -2
- package/lib/strategy-store.mjs +371 -0
- package/lib/telemetry/backends/local.mjs +6 -4
- package/lib/telemetry/client.mjs +185 -0
- package/lib/telemetry/ingest.mjs +13 -5
- package/lib/telemetry/team-rollup.mjs +9 -2
- package/lib/worker/trace.mjs +17 -27
- package/package.json +10 -2
- package/personas/construct.md +20 -20
- package/platforms/claude/settings.template.json +13 -0
- package/rules/common/research.md +44 -12
- package/scripts/sync-agents.mjs +11 -0
- package/skills/docs/backlog-proposal-workflow.md +2 -2
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +5 -5
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +3 -3
- package/skills/docs/product-signal-workflow.md +48 -18
- package/skills/docs/research-workflow.md +26 -14
- package/skills/docs/strategy-workflow.md +36 -0
- package/skills/roles/architect.ai-systems.md +4 -2
- package/skills/roles/architect.data.md +4 -2
- package/skills/roles/architect.enterprise.md +4 -2
- package/skills/roles/architect.integration.md +4 -2
- package/skills/roles/architect.md +7 -5
- package/skills/roles/architect.platform.md +4 -2
- package/skills/roles/data-analyst.experiment.md +4 -2
- package/skills/roles/data-analyst.md +9 -7
- package/skills/roles/data-analyst.product-intelligence.md +5 -3
- package/skills/roles/data-analyst.product.md +4 -2
- package/skills/roles/data-analyst.telemetry.md +4 -2
- package/skills/roles/data-engineer.pipeline.md +4 -2
- package/skills/roles/data-engineer.vector-retrieval.md +4 -2
- package/skills/roles/data-engineer.warehouse.md +4 -2
- package/skills/roles/debugger.md +7 -5
- package/skills/roles/designer.accessibility.md +4 -2
- package/skills/roles/designer.md +10 -8
- package/skills/roles/engineer.ai.md +4 -2
- package/skills/roles/engineer.data.md +5 -3
- package/skills/roles/engineer.md +14 -12
- package/skills/roles/engineer.platform.md +5 -3
- package/skills/roles/operator.docs.md +6 -4
- package/skills/roles/operator.md +6 -4
- package/skills/roles/operator.release.md +4 -2
- package/skills/roles/operator.sre.md +5 -3
- package/skills/roles/orchestrator.md +5 -3
- package/skills/roles/product-manager.ai-product.md +4 -2
- package/skills/roles/product-manager.business-strategy.md +4 -2
- package/skills/roles/product-manager.enterprise.md +4 -2
- package/skills/roles/product-manager.growth.md +4 -2
- package/skills/roles/product-manager.md +6 -4
- package/skills/roles/product-manager.platform.md +4 -2
- package/skills/roles/product-manager.product.md +4 -2
- package/skills/roles/qa.ai-eval.md +4 -2
- package/skills/roles/qa.api-contract.md +4 -2
- package/skills/roles/qa.data-pipeline.md +4 -2
- package/skills/roles/qa.md +7 -5
- package/skills/roles/qa.test-automation.md +5 -3
- package/skills/roles/qa.web-ui.md +4 -2
- package/skills/roles/researcher.explorer.md +4 -2
- package/skills/roles/researcher.md +35 -20
- package/skills/roles/researcher.ux.md +4 -2
- package/skills/roles/reviewer.devil-advocate.md +4 -2
- package/skills/roles/reviewer.evaluator.md +4 -2
- package/skills/roles/reviewer.md +14 -12
- package/skills/roles/reviewer.trace.md +4 -2
- package/skills/roles/security.ai.md +4 -2
- package/skills/roles/security.appsec.md +4 -2
- package/skills/roles/security.cloud.md +4 -2
- package/skills/roles/security.legal-compliance.md +4 -2
- package/skills/roles/security.md +7 -5
- package/skills/roles/security.privacy.md +4 -2
- package/skills/roles/security.supply-chain.md +4 -2
- package/skills/routing.md +8 -1
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/research-brief.md +63 -9
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/skill-artifact.md +27 -0
- package/templates/docs/strategy.md +36 -0
- package/templates/homebrew/construct.rb +6 -6
|
@@ -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 };
|
|
@@ -209,6 +209,7 @@ export async function verifyIntent({ request, specialist, flavor, matchedKeyword
|
|
|
209
209
|
// Kept here (not in orchestration-policy) so the classifier module stays
|
|
210
210
|
// the canonical source of "what does verified mean for this role."
|
|
211
211
|
const ROLE_PROMPT_KEYWORDS = {
|
|
212
|
+
engineer: ['ai', 'platform', 'data', 'prompt', 'pipeline', 'deploy'],
|
|
212
213
|
productManager: ['platform', 'enterprise', 'ai-product', 'growth', 'go-to-market'],
|
|
213
214
|
architect: ['ai-systems', 'integration', 'data model', 'enterprise', 'platform'],
|
|
214
215
|
qa: ['ai-eval', 'api-contract', 'data-pipeline', 'web-ui'],
|
package/lib/knowledge/layout.mjs
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
export const KNOWLEDGE_ROOT = '.cx/knowledge';
|
|
22
|
+
export const KNOWLEDGE_INTERNAL_ROOT = `${KNOWLEDGE_ROOT}/internal`;
|
|
23
|
+
export const KNOWLEDGE_REFERENCE_ROOT = `${KNOWLEDGE_ROOT}/reference`;
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* All valid knowledge subdirectory names.
|
|
@@ -70,3 +72,11 @@ export function inferKnowledgeTarget(filePath) {
|
|
|
70
72
|
if (/\bpost.?mortem\b|\bincident\b|\brca\b/.test(name)) return 'internal';
|
|
71
73
|
return 'internal';
|
|
72
74
|
}
|
|
75
|
+
|
|
76
|
+
export function knowledgeInternalStore(...segments) {
|
|
77
|
+
return ['knowledge', 'internal', ...segments].join('/');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function knowledgeReferenceStore(...segments) {
|
|
81
|
+
return ['knowledge', 'reference', ...segments].join('/');
|
|
82
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* lib/mcp/tools/telemetry.mjs — Telemetry MCP tools: trace/score, session usage, efficiency snapshot.
|
|
3
3
|
*
|
|
4
4
|
* Exposes cxTrace, cxScore, sessionUsage, and efficiencySnapshot.
|
|
5
|
-
* Requires ROOT_DIR injected via opts.
|
|
5
|
+
* Requires ROOT_DIR injected via opts. Remote export uses the shared telemetry
|
|
6
|
+
* adapter when configured; local JSONL remains available by default.
|
|
6
7
|
*/
|
|
7
8
|
import { join, resolve } from 'node:path';
|
|
8
9
|
import { homedir } from 'node:os';
|
|
9
10
|
import { readFileSync, existsSync } from 'node:fs';
|
|
10
|
-
import {
|
|
11
|
+
import { createTelemetryClient } from '../../telemetry/client.mjs';
|
|
11
12
|
import { summarizePromptComposition } from '../../prompt-composer.js';
|
|
12
13
|
import { enrichMetadataWithPrompt } from '../../prompt-metadata.mjs';
|
|
13
14
|
import { readCurrentModels, resolveExecutionContractModelMetadata, selectModelTierForWorkCategory } from '../../model-router.mjs';
|
|
@@ -16,7 +17,6 @@ import { loadWorkflow } from '../../workflow-state.mjs';
|
|
|
16
17
|
import { buildStatus } from '../../status.mjs';
|
|
17
18
|
import { readEfficiencyLog, buildCompactEfficiencyDigest } from '../../efficiency.mjs';
|
|
18
19
|
import { addObservation } from '../../observation-store.mjs';
|
|
19
|
-
// Local mock ingest client for fallback when ingestion service is unavailable
|
|
20
20
|
import { loadConstructEnv } from '../../env-config.mjs';
|
|
21
21
|
|
|
22
22
|
// Load config.env once at module init so config.env values win over shell env
|
|
@@ -34,15 +34,6 @@ function readJSON(filePath) {
|
|
|
34
34
|
|
|
35
35
|
import { execSync as _execSync } from 'node:child_process';
|
|
36
36
|
|
|
37
|
-
// Mock ingest client for fallback to direct API when the real client is not available.
|
|
38
|
-
|
|
39
|
-
function getOrCreateIngestClient() {
|
|
40
|
-
return {
|
|
41
|
-
available: false,
|
|
42
|
-
trace: () => {}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
37
|
function resolveReleaseTag(cwd) {
|
|
47
38
|
try {
|
|
48
39
|
return _execSync('git rev-parse --short HEAD', { stdio: 'pipe', cwd, timeout: 2000 }).toString().trim() || undefined;
|
|
@@ -51,18 +42,8 @@ function resolveReleaseTag(cwd) {
|
|
|
51
42
|
}
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
const secret = CONF_ENV.CONSTRUCT_TELEMETRY_SECRET_KEY ?? process.env.CONSTRUCT_TELEMETRY_SECRET_KEY;
|
|
57
|
-
if (!key || !secret) throw new Error('CONSTRUCT_TELEMETRY_PUBLIC_KEY and CONSTRUCT_TELEMETRY_SECRET_KEY must be set.');
|
|
58
|
-
return {
|
|
59
|
-
Authorization: `Basic ${Buffer.from(`${key}:${secret}`).toString('base64')}`,
|
|
60
|
-
'Content-Type': 'application/json',
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function telemetryBaseUrl() {
|
|
65
|
-
return (CONF_ENV.CONSTRUCT_TELEMETRY_URL ?? process.env.CONSTRUCT_TELEMETRY_URL ?? '').replace(/\/$/, '');
|
|
45
|
+
function telemetryEnv() {
|
|
46
|
+
return { ...process.env, ...CONF_ENV };
|
|
66
47
|
}
|
|
67
48
|
|
|
68
49
|
function resolveSessionContext() {
|
|
@@ -128,10 +109,8 @@ export async function cxTrace(args, { ROOT_DIR }) {
|
|
|
128
109
|
}, { rootDir: ROOT_DIR });
|
|
129
110
|
const traceId = args.id ?? crypto.randomUUID();
|
|
130
111
|
try {
|
|
131
|
-
const available = await telemetryAvailable();
|
|
132
|
-
if (!available) return { ok: false, error: 'Telemetry credentials not configured', id: traceId };
|
|
133
112
|
const teamId = args.metadata?.teamId ?? metadata.teamId;
|
|
134
|
-
const workCategoryValue = route?.workCategory ??
|
|
113
|
+
const workCategoryValue = route?.workCategory ?? null;
|
|
135
114
|
const body = {
|
|
136
115
|
id: traceId,
|
|
137
116
|
name: args.name,
|
|
@@ -156,27 +135,17 @@ export async function cxTrace(args, { ROOT_DIR }) {
|
|
|
156
135
|
release: ctx.release,
|
|
157
136
|
};
|
|
158
137
|
|
|
159
|
-
|
|
160
|
-
body.metadata.version = 'v1';
|
|
161
|
-
|
|
162
|
-
// Use ingestion batch for creation (handles large payloads better than direct API)
|
|
163
|
-
const ingestClient = getOrCreateIngestClient();
|
|
164
|
-
if (ingestClient?.available) {
|
|
165
|
-
ingestClient.trace(body);
|
|
166
|
-
return { ok: true, id: traceId };
|
|
167
|
-
}
|
|
138
|
+
body.metadata.version = 'v1';
|
|
168
139
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
return { ok: true, id: traceId };
|
|
140
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: ROOT_DIR });
|
|
141
|
+
client.trace(body);
|
|
142
|
+
await client.flush();
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
id: traceId,
|
|
146
|
+
backend: client.backend,
|
|
147
|
+
remoteStatus: client.remoteStatus,
|
|
148
|
+
};
|
|
180
149
|
} catch (err) {
|
|
181
150
|
return { ok: false, error: err.message, id: traceId };
|
|
182
151
|
}
|
|
@@ -193,25 +162,16 @@ export async function cxTraceUpdate(args) {
|
|
|
193
162
|
const metadata = args.metadata;
|
|
194
163
|
|
|
195
164
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
metadata: {
|
|
205
|
-
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
206
|
-
traceUpdatedAt: new Date().toISOString(),
|
|
207
|
-
},
|
|
208
|
-
}),
|
|
165
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: process.cwd() });
|
|
166
|
+
client.traceUpdate({
|
|
167
|
+
id: traceId,
|
|
168
|
+
output,
|
|
169
|
+
metadata: {
|
|
170
|
+
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
171
|
+
traceUpdatedAt: new Date().toISOString(),
|
|
172
|
+
},
|
|
209
173
|
});
|
|
210
|
-
|
|
211
|
-
if (!res.ok) {
|
|
212
|
-
const text = await res.text().catch(() => '');
|
|
213
|
-
return { ok: false, error: `Telemetry PATCH error ${res.status}: ${text}` };
|
|
214
|
-
}
|
|
174
|
+
await client.flush();
|
|
215
175
|
|
|
216
176
|
// Also record as observation for local learning
|
|
217
177
|
if (output || metadata) {
|
|
@@ -227,7 +187,7 @@ export async function cxTraceUpdate(args) {
|
|
|
227
187
|
});
|
|
228
188
|
}
|
|
229
189
|
|
|
230
|
-
return { ok: true, traceId };
|
|
190
|
+
return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
|
|
231
191
|
} catch (err) {
|
|
232
192
|
return { ok: false, error: err.message };
|
|
233
193
|
}
|
|
@@ -240,8 +200,6 @@ const SCORE_GOOD_THRESHOLD = 0.85; // at or above this → record positive pat
|
|
|
240
200
|
export async function cxScore(args) {
|
|
241
201
|
const traceId = args.trace_id ?? '';
|
|
242
202
|
try {
|
|
243
|
-
const available = await telemetryAvailable();
|
|
244
|
-
if (!available) return { ok: false, error: 'Telemetry credentials not configured' };
|
|
245
203
|
const body = {
|
|
246
204
|
id: crypto.randomUUID(),
|
|
247
205
|
traceId,
|
|
@@ -250,15 +208,9 @@ export async function cxScore(args) {
|
|
|
250
208
|
dataType: 'NUMERIC',
|
|
251
209
|
comment: args.comment,
|
|
252
210
|
};
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
body: JSON.stringify(body),
|
|
257
|
-
});
|
|
258
|
-
if (!res.ok) {
|
|
259
|
-
const text = await res.text().catch(() => '');
|
|
260
|
-
return { ok: false, error: `Telemetry API error ${res.status}: ${text}` };
|
|
261
|
-
}
|
|
211
|
+
const client = createTelemetryClient({ env: telemetryEnv(), rootDir: process.cwd() });
|
|
212
|
+
client.score(body);
|
|
213
|
+
await client.flush();
|
|
262
214
|
|
|
263
215
|
// Feed score back into the local observation store so future agents learn from it.
|
|
264
216
|
// Low scores generate anti-pattern observations; high scores reinforce positive patterns.
|
|
@@ -291,7 +243,7 @@ export async function cxScore(args) {
|
|
|
291
243
|
}
|
|
292
244
|
}
|
|
293
245
|
|
|
294
|
-
return { ok: true, traceId };
|
|
246
|
+
return { ok: true, traceId, backend: client.backend, remoteStatus: client.remoteStatus };
|
|
295
247
|
} catch (err) {
|
|
296
248
|
return { ok: false, error: err.message };
|
|
297
249
|
}
|
package/lib/model-router.mjs
CHANGED
|
@@ -45,6 +45,61 @@ export const MODEL_TIER_BY_WORK_CATEGORY = {
|
|
|
45
45
|
analysis: "standard",
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
export const MODEL_OPERATING_PROFILES = Object.freeze({
|
|
49
|
+
balanced: {
|
|
50
|
+
id: 'balanced',
|
|
51
|
+
label: 'Balanced',
|
|
52
|
+
maxPromptTokens: 3000,
|
|
53
|
+
learnedPatternsTokens: 200,
|
|
54
|
+
taskPacketTokens: 150,
|
|
55
|
+
contextDigestTokens: 200,
|
|
56
|
+
hostConstraintsTokens: 75,
|
|
57
|
+
roleFlavorTokens: 600,
|
|
58
|
+
retrievalFirst: false,
|
|
59
|
+
preferCompressedRoleGuidance: false,
|
|
60
|
+
},
|
|
61
|
+
small: {
|
|
62
|
+
id: 'small',
|
|
63
|
+
label: 'Small-model',
|
|
64
|
+
maxPromptTokens: 1800,
|
|
65
|
+
learnedPatternsTokens: 120,
|
|
66
|
+
taskPacketTokens: 110,
|
|
67
|
+
contextDigestTokens: 120,
|
|
68
|
+
hostConstraintsTokens: 40,
|
|
69
|
+
roleFlavorTokens: 280,
|
|
70
|
+
retrievalFirst: true,
|
|
71
|
+
preferCompressedRoleGuidance: true,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function normalizeModelOperatingProfile(value) {
|
|
76
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
77
|
+
if (!normalized) return null;
|
|
78
|
+
if (normalized === 'default') return 'balanced';
|
|
79
|
+
return MODEL_OPERATING_PROFILES[normalized] ? normalized : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function inferSmallModelProfile(selectedModel) {
|
|
83
|
+
const model = String(selectedModel || '').toLowerCase();
|
|
84
|
+
if (!model) return false;
|
|
85
|
+
if (/^(ollama|local)\//.test(model) && /(?:[:/-])(3b|7b|8b|13b|14b|32b)\b/.test(model)) return true;
|
|
86
|
+
if (/^(anthropic|openrouter\/anthropic)\/.*haiku/.test(model)) return true;
|
|
87
|
+
if (/gpt-5\.1-mini|gemma-3|gemma-4|phi3:mini/.test(model)) return true;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resolveModelOperatingProfile({
|
|
92
|
+
envValues = {},
|
|
93
|
+
selectedModel = null,
|
|
94
|
+
} = {}) {
|
|
95
|
+
const explicit = normalizeModelOperatingProfile(
|
|
96
|
+
envValues.CONSTRUCT_MODEL_PROFILE ?? envValues.constructModelProfile
|
|
97
|
+
);
|
|
98
|
+
if (explicit) return MODEL_OPERATING_PROFILES[explicit];
|
|
99
|
+
if (inferSmallModelProfile(selectedModel)) return MODEL_OPERATING_PROFILES.small;
|
|
100
|
+
return MODEL_OPERATING_PROFILES.balanced;
|
|
101
|
+
}
|
|
102
|
+
|
|
48
103
|
/**
|
|
49
104
|
* Provider-family definitions. Each entry contains:
|
|
50
105
|
* - `test`: RegExp that matches provider URLs.
|
|
@@ -229,7 +284,7 @@ const PROVIDER_ENV_MAP = {
|
|
|
229
284
|
'github-copilot': ['GITHUB_TOKEN', 'GH_TOKEN'],
|
|
230
285
|
'openai': ['OPENAI_API_KEY'],
|
|
231
286
|
'openrouter-llama': ['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'],
|
|
232
|
-
'ollama': ['OLLAMA_BASE_URL'],
|
|
287
|
+
'ollama': ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
|
|
233
288
|
'local': ['LOCAL_LLM_BASE_URL'],
|
|
234
289
|
};
|
|
235
290
|
|
|
@@ -630,6 +685,10 @@ export function resolveExecutionContractModelMetadata({
|
|
|
630
685
|
const tiers = resolveTierAssignments(envValues, registryModels);
|
|
631
686
|
const selectedTier = requestedTier ?? selectModelTierForWorkCategory(workCategory);
|
|
632
687
|
const selected = selectedTier ? tiers[selectedTier] : null;
|
|
688
|
+
const profile = resolveModelOperatingProfile({
|
|
689
|
+
envValues,
|
|
690
|
+
selectedModel: selected?.model ?? null,
|
|
691
|
+
});
|
|
633
692
|
|
|
634
693
|
return {
|
|
635
694
|
version: "v1",
|
|
@@ -638,6 +697,7 @@ export function resolveExecutionContractModelMetadata({
|
|
|
638
697
|
selectedTier: selectedTier ?? null,
|
|
639
698
|
selectedModel: selected?.model ?? null,
|
|
640
699
|
selectedModelSource: selected?.source ?? null,
|
|
700
|
+
profile,
|
|
641
701
|
tiers,
|
|
642
702
|
};
|
|
643
703
|
}
|
|
@@ -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
|
};
|
package/lib/ollama-manager.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { execSync } from 'node:child_process';
|
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
|
|
13
|
-
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
13
|
+
const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL || process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Check if Ollama is installed and running
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* trace/generation/span/event observations, and flushes on session.idle.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import {
|
|
8
|
+
import { createTelemetryClient } from "./telemetry/client.mjs";
|
|
9
9
|
|
|
10
10
|
const SECRET_PATH_PATTERNS = [
|
|
11
11
|
/(^|\/)\.env(\.|$)/i,
|
|
@@ -24,10 +24,9 @@ let cachedIngest = null;
|
|
|
24
24
|
|
|
25
25
|
export function getIngestClient(env = process.env) {
|
|
26
26
|
if (cachedIngest) return cachedIngest;
|
|
27
|
-
cachedIngest =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
secretKey: env.CONSTRUCT_TELEMETRY_SECRET_KEY,
|
|
27
|
+
cachedIngest = createTelemetryClient({
|
|
28
|
+
env,
|
|
29
|
+
rootDir: env.CX_TOOLKIT_DIR,
|
|
31
30
|
onError: (err) => {
|
|
32
31
|
if (env.CONSTRUCT_TRACE_DEBUG === "1") {
|
|
33
32
|
// eslint-disable-next-line no-console
|