@geraldmaron/construct 1.0.4 → 1.0.6
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 +57 -44
- package/agents/prompts/cx-accessibility.md +3 -3
- package/agents/prompts/cx-ai-engineer.md +7 -7
- package/agents/prompts/cx-architect.md +6 -6
- package/agents/prompts/cx-business-strategist.md +6 -6
- package/agents/prompts/cx-data-analyst.md +7 -7
- package/agents/prompts/cx-data-engineer.md +3 -3
- package/agents/prompts/cx-debugger.md +6 -6
- package/agents/prompts/cx-designer.md +5 -5
- package/agents/prompts/cx-devil-advocate.md +4 -4
- package/agents/prompts/cx-docs-keeper.md +5 -5
- package/agents/prompts/cx-engineer.md +5 -5
- package/agents/prompts/cx-evaluator.md +2 -2
- package/agents/prompts/cx-explorer.md +8 -8
- package/agents/prompts/cx-legal-compliance.md +2 -2
- package/agents/prompts/cx-operations.md +5 -5
- package/agents/prompts/cx-orchestrator.md +10 -10
- package/agents/prompts/cx-platform-engineer.md +5 -5
- package/agents/prompts/cx-product-manager.md +4 -4
- package/agents/prompts/cx-qa.md +8 -8
- package/agents/prompts/cx-rd-lead.md +9 -9
- package/agents/prompts/cx-release-manager.md +5 -5
- package/agents/prompts/cx-researcher.md +22 -22
- package/agents/prompts/cx-reviewer.md +7 -7
- package/agents/prompts/cx-security.md +10 -10
- package/agents/prompts/cx-sre.md +7 -7
- package/agents/prompts/cx-test-automation.md +3 -3
- package/agents/prompts/cx-trace-reviewer.md +8 -8
- package/agents/prompts/cx-ux-researcher.md +3 -3
- package/bin/construct +470 -4
- package/commands/build/feature.md +4 -4
- package/commands/build/fix.md +8 -8
- package/commands/design/access.md +3 -3
- package/commands/design/flow.md +3 -3
- package/commands/design/ui.md +4 -4
- package/commands/measure/experiment.md +5 -5
- package/commands/measure/metrics.md +3 -3
- package/commands/measure/results.md +4 -4
- package/commands/plan/api.md +3 -3
- package/commands/plan/challenge.md +3 -3
- package/commands/plan/decide.md +3 -3
- package/commands/plan/feature.md +7 -7
- package/commands/plan/requirements.md +3 -3
- package/commands/remember/context.md +5 -5
- package/commands/remember/handoff.md +3 -3
- package/commands/remember/runbook.md +3 -3
- package/commands/review/code.md +8 -8
- package/commands/review/quality.md +4 -4
- package/commands/review/security.md +3 -3
- package/commands/ship/ready.md +3 -3
- package/commands/ship/release.md +3 -3
- package/commands/ship/status.md +4 -4
- package/commands/understand/docs.md +3 -3
- package/commands/understand/research.md +5 -3
- package/commands/understand/this.md +4 -4
- package/commands/understand/why.md +8 -8
- package/commands/work/clean.md +14 -14
- package/commands/work/drive.md +10 -10
- package/commands/work/optimize-prompts.md +9 -9
- package/commands/work/parallel-review.md +8 -8
- package/db/schema/006_graph.sql +24 -0
- package/examples/provider-plugin/README.md +7 -7
- package/examples/seed-observations/README.md +6 -6
- package/examples/seed-observations/anti-patterns.md +14 -14
- package/examples/seed-observations/decisions.md +4 -4
- package/examples/seed-observations/patterns.md +14 -14
- package/lib/auto-docs.mjs +10 -5
- package/lib/cli-commands.mjs +45 -1
- package/lib/comment-lint.mjs +7 -1
- package/lib/config/schema.mjs +3 -0
- package/lib/flavors/loader.mjs +136 -0
- package/lib/hooks/agent-tracker.mjs +22 -3
- package/lib/hooks/pre-push-gate.mjs +14 -1
- package/lib/hooks/session-optimize.mjs +3 -2
- package/lib/hooks/session-reflect.mjs +68 -0
- package/lib/init-unified.mjs +25 -2
- 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/knowledge/graph.mjs +213 -0
- package/lib/knowledge/research-store.mjs +109 -0
- package/lib/mcp/server.mjs +187 -1
- package/lib/mcp/tools/profile.mjs +270 -0
- package/lib/observation-store.mjs +19 -0
- package/lib/outcomes/aggregate.mjs +104 -0
- package/lib/outcomes/record.mjs +115 -0
- package/lib/parity.mjs +6 -9
- package/lib/profiles/lifecycle.mjs +388 -0
- package/lib/profiles/loader.mjs +123 -0
- package/lib/profiles/validate-custom.mjs +114 -0
- package/lib/reflect/extractor.mjs +193 -0
- package/lib/reflect.mjs +89 -2
- package/lib/sandbox.mjs +102 -0
- package/package.json +6 -1
- package/personas/construct.md +20 -20
- package/platforms/claude/CLAUDE.md +6 -6
- package/platforms/claude/settings.template.json +13 -0
- package/rules/common/agents.md +2 -2
- package/rules/common/beads-hygiene.md +11 -11
- package/rules/common/code-review.md +1 -1
- package/rules/common/coding-style.md +1 -1
- package/rules/common/comments.md +8 -8
- package/rules/common/commit-approval.md +4 -4
- package/rules/common/cx-agent-routing.md +2 -2
- package/rules/common/cx-skill-routing.md +2 -2
- package/rules/common/development-workflow.md +1 -1
- package/rules/common/doc-ownership.md +2 -2
- package/rules/common/efficiency.md +3 -3
- package/rules/common/framing.md +1 -1
- package/rules/common/git-workflow.md +1 -1
- package/rules/common/patterns.md +1 -1
- package/rules/common/performance.md +1 -1
- package/rules/common/release-gates.md +7 -7
- package/rules/common/research.md +4 -4
- package/rules/common/security.md +1 -1
- package/rules/common/skill-composition.md +8 -8
- package/rules/common/testing.md +1 -1
- package/rules/golang/coding-style.md +2 -2
- package/rules/golang/hooks.md +1 -1
- package/rules/golang/patterns.md +1 -1
- package/rules/golang/security.md +1 -1
- package/rules/golang/testing.md +1 -1
- package/rules/python/coding-style.md +1 -1
- package/rules/python/hooks.md +1 -1
- package/rules/python/patterns.md +1 -1
- package/rules/python/security.md +1 -1
- package/rules/python/testing.md +1 -1
- package/rules/swift/coding-style.md +3 -3
- package/rules/swift/hooks.md +2 -2
- package/rules/swift/patterns.md +2 -2
- package/rules/swift/security.md +4 -4
- package/rules/swift/testing.md +2 -2
- package/rules/typescript/coding-style.md +1 -1
- package/rules/typescript/hooks.md +1 -1
- package/rules/typescript/patterns.md +1 -1
- package/rules/typescript/security.md +1 -1
- package/rules/typescript/testing.md +1 -1
- package/rules/web/coding-style.md +1 -1
- package/rules/web/design-quality.md +1 -1
- package/rules/web/hooks.md +1 -1
- package/rules/web/patterns.md +1 -1
- package/rules/web/performance.md +1 -1
- package/rules/web/security.md +1 -1
- package/rules/web/testing.md +1 -1
- package/scripts/sync-agents.mjs +11 -0
- package/skills/ai/agent-dev.md +1 -1
- package/skills/ai/llm-security.md +1 -1
- package/skills/ai/ml-ops.md +6 -6
- package/skills/ai/orchestration-workflow.md +1 -1
- package/skills/ai/prompt-and-eval.md +1 -1
- package/skills/ai/prompt-optimizer.md +13 -13
- package/skills/ai/rag-system.md +1 -1
- package/skills/architecture/api-design.md +1 -1
- package/skills/architecture/caching.md +1 -1
- package/skills/architecture/cloud-native.md +1 -1
- package/skills/architecture/message-queue.md +1 -1
- package/skills/architecture/security-arch.md +1 -1
- package/skills/compliance/ai-disclosure.md +1 -1
- package/skills/compliance/data-privacy.md +1 -1
- package/skills/compliance/license-audit.md +2 -2
- package/skills/compliance/regulatory-review.md +1 -1
- package/skills/development/cpp.md +1 -1
- package/skills/development/go.md +1 -1
- package/skills/development/java.md +1 -1
- package/skills/development/kotlin.md +9 -9
- package/skills/development/mobile-crossplatform.md +13 -13
- package/skills/development/python.md +1 -1
- package/skills/development/rust.md +1 -1
- package/skills/development/shell.md +1 -1
- package/skills/development/swift.md +6 -6
- package/skills/development/typescript.md +1 -1
- package/skills/devops/ci-cd.md +5 -5
- package/skills/devops/containerization.md +9 -9
- package/skills/devops/cost-optimization.md +1 -1
- package/skills/devops/data-engineering.md +2 -2
- package/skills/devops/database.md +1 -1
- package/skills/devops/dependency-management.md +3 -3
- package/skills/devops/devsecops.md +1 -1
- package/skills/devops/git-workflow.md +1 -1
- package/skills/devops/incident-response.md +18 -18
- package/skills/devops/monorepo.md +5 -5
- package/skills/devops/observability.md +1 -1
- package/skills/devops/performance.md +1 -1
- package/skills/devops/testing.md +1 -1
- package/skills/docs/adr-workflow.md +2 -2
- package/skills/docs/backlog-proposal-workflow.md +1 -1
- package/skills/docs/customer-profile-workflow.md +1 -1
- package/skills/docs/document-ingest-workflow.md +1 -1
- package/skills/docs/evidence-ingest-workflow.md +1 -1
- package/skills/docs/init-docs.md +15 -15
- package/skills/docs/init-project.md +1 -1
- package/skills/docs/prd-workflow.md +3 -3
- package/skills/docs/prfaq-workflow.md +1 -1
- package/skills/docs/product-intelligence-review.md +1 -1
- package/skills/docs/product-intelligence-workflow.md +1 -1
- package/skills/docs/product-signal-workflow.md +9 -9
- package/skills/docs/research-workflow.md +10 -10
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/strategy-workflow.md +3 -3
- package/skills/exploration/repo-map.md +11 -11
- package/skills/frameworks/django.md +15 -15
- package/skills/frameworks/nextjs.md +16 -16
- package/skills/frameworks/react.md +12 -12
- package/skills/frameworks/spring-boot.md +12 -12
- package/skills/frontend-design/accessibility.md +6 -6
- package/skills/frontend-design/component-patterns.md +1 -1
- package/skills/frontend-design/engineering.md +1 -1
- package/skills/frontend-design/state-management.md +1 -1
- package/skills/frontend-design/ui-aesthetics.md +1 -1
- package/skills/frontend-design/ux-principles.md +1 -1
- package/skills/operating/orchestration-reference.md +27 -27
- package/skills/quality-gates/review-work.md +3 -3
- package/skills/quality-gates/verify-change.md +1 -1
- package/skills/quality-gates/verify-module.md +1 -1
- package/skills/quality-gates/verify-quality.md +1 -1
- package/skills/quality-gates/verify-security.md +1 -1
- 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 +4 -2
- 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 +11 -9
- 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 +14 -14
- package/skills/security/blue-team.md +1 -1
- package/skills/security/code-audit.md +1 -1
- package/skills/security/pentest.md +1 -1
- package/skills/security/red-team.md +1 -1
- package/skills/security/threat-intel.md +1 -1
- package/skills/security/vuln-research.md +1 -1
- package/skills/utility/clean-code.md +2 -2
- package/templates/docs/changelog-entry.md +1 -1
- package/templates/docs/construct_guide.md +13 -13
- package/templates/docs/meta-prd.md +16 -16
- package/templates/docs/one-pager.md +1 -1
- package/templates/docs/persona-artifact.md +36 -0
- package/templates/docs/prd-business.md +1 -1
- package/templates/docs/prd-platform.md +1 -1
- package/templates/docs/prd.md +17 -17
- package/templates/docs/research-brief.md +8 -8
- package/templates/docs/research-finding.md +26 -0
- package/templates/docs/rfc.md +1 -1
- package/templates/docs/skill-artifact.md +27 -0
- package/templates/docs/strategy.md +1 -1
|
@@ -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,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/knowledge/graph.mjs — GraphRAG-style entity index + global query.
|
|
3
|
+
*
|
|
4
|
+
* Phase C9 foundations:
|
|
5
|
+
* - buildGraph(rootDir): derive nodes + edges from `.cx/observations/entities.json`.
|
|
6
|
+
* - detectCommunities(graph): pure-JS label propagation. Deterministic
|
|
7
|
+
* (sorted neighbor scan, lowest-id tiebreak). O(V+E) per iteration.
|
|
8
|
+
* - summarizeCommunity(community, entities): extractive summary over the
|
|
9
|
+
* member entities, ranked by degree centrality. Requires no LLM call,
|
|
10
|
+
* so the primitive works in solo mode without a provider configured.
|
|
11
|
+
* - askGlobal({ query, rootDir }): rank communities by overlap with the
|
|
12
|
+
* query terms + their members' search text, return the top communities
|
|
13
|
+
* with member entities and the question they best answer.
|
|
14
|
+
*
|
|
15
|
+
* Full Leiden (modularity optimization + refinement) would need an external
|
|
16
|
+
* graph library and is blocked by ADR-0001 (zero-npm-core). Label propagation
|
|
17
|
+
* is the deterministic alternative. Produces useful coarse communities on
|
|
18
|
+
* the small graphs Construct sessions accumulate (typically <500 entities).
|
|
19
|
+
* Swap implementations later by replacing `detectCommunities` only.
|
|
20
|
+
*/
|
|
21
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { rankByBm25 } from '../storage/embeddings.mjs';
|
|
24
|
+
|
|
25
|
+
const OBS_DIR = '.cx/observations';
|
|
26
|
+
const ENTITIES_FILE = 'entities.json';
|
|
27
|
+
|
|
28
|
+
const MAX_LP_ITERATIONS = 30;
|
|
29
|
+
const MAX_COMMUNITY_TOP_TERMS = 8;
|
|
30
|
+
const MIN_COMMUNITY_SIZE = 2;
|
|
31
|
+
|
|
32
|
+
function readEntities(rootDir) {
|
|
33
|
+
const p = path.join(rootDir, OBS_DIR, ENTITIES_FILE);
|
|
34
|
+
if (!existsSync(p)) return [];
|
|
35
|
+
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return []; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build an undirected graph from the entity store. Nodes are entity names;
|
|
40
|
+
* edges come from `relatedEntities[]`. Returns:
|
|
41
|
+
* { nodes: Map<name, entity>, adj: Map<name, Set<name>> }
|
|
42
|
+
*/
|
|
43
|
+
export function buildGraph(rootDir) {
|
|
44
|
+
const entities = readEntities(rootDir);
|
|
45
|
+
const nodes = new Map();
|
|
46
|
+
const adj = new Map();
|
|
47
|
+
for (const e of entities) {
|
|
48
|
+
if (!e?.name) continue;
|
|
49
|
+
nodes.set(e.name, e);
|
|
50
|
+
if (!adj.has(e.name)) adj.set(e.name, new Set());
|
|
51
|
+
}
|
|
52
|
+
for (const e of entities) {
|
|
53
|
+
if (!e?.name) continue;
|
|
54
|
+
for (const other of e.relatedEntities || []) {
|
|
55
|
+
if (!nodes.has(other)) continue;
|
|
56
|
+
adj.get(e.name).add(other);
|
|
57
|
+
adj.get(other).add(e.name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { nodes, adj };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run synchronous label propagation. Each node starts with its own label;
|
|
65
|
+
* iteratively adopt the most frequent label among neighbors, with a sorted
|
|
66
|
+
* neighbor scan and lowest-id tiebreak so the result is deterministic.
|
|
67
|
+
*
|
|
68
|
+
* Returns: Map<name, communityId> plus a stable grouping.
|
|
69
|
+
*/
|
|
70
|
+
export function detectCommunities(graph) {
|
|
71
|
+
const labels = new Map();
|
|
72
|
+
const sortedNodes = [...graph.nodes.keys()].sort();
|
|
73
|
+
for (const n of sortedNodes) labels.set(n, n);
|
|
74
|
+
|
|
75
|
+
for (let iter = 0; iter < MAX_LP_ITERATIONS; iter++) {
|
|
76
|
+
let changed = false;
|
|
77
|
+
for (const node of sortedNodes) {
|
|
78
|
+
const neighbors = [...(graph.adj.get(node) || [])].sort();
|
|
79
|
+
if (neighbors.length === 0) continue;
|
|
80
|
+
|
|
81
|
+
const counts = new Map();
|
|
82
|
+
for (const nb of neighbors) {
|
|
83
|
+
const lbl = labels.get(nb);
|
|
84
|
+
counts.set(lbl, (counts.get(lbl) || 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// highest-count label, lowest-id tiebreak
|
|
88
|
+
let bestLabel = labels.get(node);
|
|
89
|
+
let bestCount = -1;
|
|
90
|
+
const sortedLabels = [...counts.entries()].sort((a, b) => {
|
|
91
|
+
if (b[1] !== a[1]) return b[1] - a[1];
|
|
92
|
+
return a[0] < b[0] ? -1 : 1;
|
|
93
|
+
});
|
|
94
|
+
if (sortedLabels.length > 0) {
|
|
95
|
+
[bestLabel, bestCount] = sortedLabels[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (bestCount > 0 && bestLabel !== labels.get(node)) {
|
|
99
|
+
labels.set(node, bestLabel);
|
|
100
|
+
changed = true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!changed) break;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const communities = new Map();
|
|
107
|
+
for (const [node, label] of labels) {
|
|
108
|
+
if (!communities.has(label)) communities.set(label, []);
|
|
109
|
+
communities.get(label).push(node);
|
|
110
|
+
}
|
|
111
|
+
// Stable order: by size desc, then alphabetical on canonical label
|
|
112
|
+
const grouped = [...communities.entries()]
|
|
113
|
+
.map(([id, members]) => ({ id, members: members.sort() }))
|
|
114
|
+
.sort((a, b) => {
|
|
115
|
+
if (b.members.length !== a.members.length) return b.members.length - a.members.length;
|
|
116
|
+
return a.id < b.id ? -1 : 1;
|
|
117
|
+
});
|
|
118
|
+
return { labels, communities: grouped };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Extractive community summary. Picks the top-N members by intra-community
|
|
123
|
+
* degree (the "central" nodes of the cluster) and harvests their summaries
|
|
124
|
+
* into a short text block. Deterministic and dependency-free.
|
|
125
|
+
*/
|
|
126
|
+
export function summarizeCommunity(group, graph) {
|
|
127
|
+
if (!group?.members?.length) return { id: group?.id, size: 0, topMembers: [], summary: '' };
|
|
128
|
+
const memberSet = new Set(group.members);
|
|
129
|
+
const scored = group.members.map((name) => {
|
|
130
|
+
const neighbors = graph.adj.get(name) || new Set();
|
|
131
|
+
let degree = 0;
|
|
132
|
+
for (const nb of neighbors) if (memberSet.has(nb)) degree++;
|
|
133
|
+
return { name, degree };
|
|
134
|
+
}).sort((a, b) => {
|
|
135
|
+
if (b.degree !== a.degree) return b.degree - a.degree;
|
|
136
|
+
return a.name < b.name ? -1 : 1;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const topMembers = scored.slice(0, MAX_COMMUNITY_TOP_TERMS);
|
|
140
|
+
const lines = [];
|
|
141
|
+
for (const { name } of topMembers) {
|
|
142
|
+
const entity = graph.nodes.get(name);
|
|
143
|
+
if (!entity) continue;
|
|
144
|
+
const summary = (entity.summary || '').trim();
|
|
145
|
+
if (summary) lines.push(`- ${entity.name}: ${summary}`);
|
|
146
|
+
else lines.push(`- ${entity.name}`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
id: group.id,
|
|
150
|
+
size: group.members.length,
|
|
151
|
+
topMembers: topMembers.map((m) => m.name),
|
|
152
|
+
summary: lines.join('\n'),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Global query over the entity graph. Builds the graph, detects communities,
|
|
158
|
+
* scores each community by BM25 over its concatenated member summaries, and
|
|
159
|
+
* returns the top communities with members.
|
|
160
|
+
*
|
|
161
|
+
* Intentionally returns structured data instead of a synthesized prose
|
|
162
|
+
* answer: the LM-based response synthesis is the next layer up and belongs
|
|
163
|
+
* in the persona prompt, not in this retrieval primitive. Solo mode can
|
|
164
|
+
* consume the raw result without calling an external model.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} args
|
|
167
|
+
* @param {string} args.query - natural-language question
|
|
168
|
+
* @param {string} args.rootDir - project root containing `.cx/observations/`
|
|
169
|
+
* @param {number} [args.topK=5] - max communities to return
|
|
170
|
+
* @param {number} [args.minSize=MIN_COMMUNITY_SIZE] - skip singleton communities
|
|
171
|
+
* @returns {{ query, communities: Array<{ id, size, topMembers, summary, score }>, totalEntities, totalCommunities }}
|
|
172
|
+
*/
|
|
173
|
+
export function askGlobal({ query, rootDir, topK = 5, minSize = MIN_COMMUNITY_SIZE } = {}) {
|
|
174
|
+
if (!query || typeof query !== 'string') {
|
|
175
|
+
return { query: query || '', communities: [], totalEntities: 0, totalCommunities: 0 };
|
|
176
|
+
}
|
|
177
|
+
const graph = buildGraph(rootDir);
|
|
178
|
+
if (graph.nodes.size === 0) {
|
|
179
|
+
return { query, communities: [], totalEntities: 0, totalCommunities: 0 };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const { communities } = detectCommunities(graph);
|
|
183
|
+
const eligible = communities.filter((g) => g.members.length >= minSize);
|
|
184
|
+
if (eligible.length === 0) {
|
|
185
|
+
return {
|
|
186
|
+
query,
|
|
187
|
+
communities: [],
|
|
188
|
+
totalEntities: graph.nodes.size,
|
|
189
|
+
totalCommunities: communities.length,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const summaries = eligible.map((group) => summarizeCommunity(group, graph));
|
|
194
|
+
const docs = summaries.map((s, i) => ({
|
|
195
|
+
id: String(i),
|
|
196
|
+
text: `${s.topMembers.join(' ')}\n${s.summary}`,
|
|
197
|
+
}));
|
|
198
|
+
const ranked = rankByBm25(docs, query, { limit: topK });
|
|
199
|
+
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const r of ranked) {
|
|
202
|
+
const summary = summaries[Number(r.id)];
|
|
203
|
+
if (!summary) continue;
|
|
204
|
+
out.push({ ...summary, score: r.score });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
query,
|
|
209
|
+
communities: out,
|
|
210
|
+
totalEntities: graph.nodes.size,
|
|
211
|
+
totalCommunities: communities.length,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -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
|
+
}
|