@taodigital/agents 0.0.0-stage → 1.2.0
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 +99 -2
- package/dist/api-test-agent.d.ts +126 -0
- package/dist/api-test-agent.js +267 -0
- package/dist/artifact-intel-agent.d.ts +11 -0
- package/dist/artifact-intel-agent.js +152 -0
- package/dist/coverage-gap-agent.d.ts +25 -0
- package/dist/coverage-gap-agent.js +269 -0
- package/dist/coverage-map-agent.d.ts +15 -0
- package/dist/coverage-map-agent.js +255 -0
- package/dist/crawl-diff-agent.d.ts +27 -0
- package/dist/crawl-diff-agent.js +412 -0
- package/dist/data-agent.d.ts +101 -0
- package/dist/data-agent.js +2284 -0
- package/dist/document-agent.d.ts +10 -0
- package/dist/document-agent.js +32 -0
- package/dist/enhancement-agent.d.ts +48 -0
- package/dist/enhancement-agent.js +342 -0
- package/dist/field-detector-agent.d.ts +35 -0
- package/dist/field-detector-agent.js +220 -0
- package/dist/flow-agent.d.ts +130 -0
- package/dist/flow-agent.js +169 -0
- package/dist/github-agent.d.ts +18 -0
- package/dist/github-agent.js +39 -0
- package/dist/github-write-agent.d.ts +67 -0
- package/dist/github-write-agent.js +249 -0
- package/dist/grouping-agent.d.ts +18 -0
- package/dist/grouping-agent.js +338 -0
- package/dist/healing-agent.d.ts +73 -0
- package/dist/healing-agent.js +137 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +67 -0
- package/dist/integration-test-agent.d.ts +118 -0
- package/dist/integration-test-agent.js +165 -0
- package/dist/label-assignment-agent.d.ts +15 -0
- package/dist/label-assignment-agent.js +97 -0
- package/dist/llm-config.d.ts +38 -0
- package/dist/llm-config.js +287 -0
- package/dist/llm-mock.d.ts +77 -0
- package/dist/llm-mock.js +192 -0
- package/dist/llm.d.ts +58 -0
- package/dist/llm.js +438 -0
- package/dist/nlp-agent.d.ts +208 -0
- package/dist/nlp-agent.js +340 -0
- package/dist/orchestrator.d.ts +285 -0
- package/dist/orchestrator.js +141 -0
- package/dist/plan-agent.d.ts +4 -0
- package/dist/plan-agent.js +200 -0
- package/dist/repo-scan-agent.d.ts +189 -0
- package/dist/repo-scan-agent.js +2032 -0
- package/dist/requirements-agent.d.ts +59 -0
- package/dist/requirements-agent.js +641 -0
- package/dist/scenario-agent.d.ts +20 -0
- package/dist/scenario-agent.js +123 -0
- package/dist/script-agent.d.ts +447 -0
- package/dist/script-agent.js +11813 -0
- package/dist/security-test-agent.d.ts +111 -0
- package/dist/security-test-agent.js +141 -0
- package/dist/skill-file-agent.d.ts +264 -0
- package/dist/skill-file-agent.js +3470 -0
- package/dist/step-coverage.d.ts +46 -0
- package/dist/step-coverage.js +288 -0
- package/dist/step-utils.d.ts +2 -0
- package/dist/step-utils.js +62 -0
- package/dist/test-case-agent.d.ts +206 -0
- package/dist/test-case-agent.js +2141 -0
- package/dist/test-import-agent.d.ts +148 -0
- package/dist/test-import-agent.js +3235 -0
- package/dist/test-script-agent.d.ts +16 -0
- package/dist/test-script-agent.js +48 -0
- package/dist/token-pricing.d.ts +14 -0
- package/dist/token-pricing.js +62 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.js +7 -0
- package/dist/visual-agent.d.ts +114 -0
- package/dist/visual-agent.js +261 -0
- package/package.json +26 -4
package/README.md
CHANGED
|
@@ -1,3 +1,100 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @detiq/agents — Generation Agents & E2E Evaluation Harness
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
AI agents that produce test artifacts grounded in the ZeTa Core, coordinated by orchestrators:
|
|
4
|
+
- `DualContextOrchestrator` / `SwarmOrchestrator`: Multi-agent pipeline coordinating CAG (Component-Augmented Generation) and KAG (Knowledge-Augmented Generation).
|
|
5
|
+
- `test-case-agent.ts`: 22-pass test case generation grounded in real crawled elements with semantic dedup.
|
|
6
|
+
- `data-agent.ts`: Realistic synthetic test data and slot generation matching field constraints.
|
|
7
|
+
- `script-agent.ts`: Deterministic Playwright test script generation and linting.
|
|
8
|
+
- `alignment-agent.ts`: Verification of script actions against test case steps.
|
|
9
|
+
- `llm.ts`: Choke-point wrapper around LLM providers with token tracking, caching, and error sanitization.
|
|
10
|
+
- `llm-mock.ts`: Mock / record / replay engine for zero-cost, deterministic CI runs.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## E2E Evaluation Harness (`scripts/agent-e2e.ts`)
|
|
15
|
+
|
|
16
|
+
The E2E harness tests the entire agent pipeline from screen fixture to verified Playwright script without requiring live browser crawling or UI clicks.
|
|
17
|
+
|
|
18
|
+
### Commands
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 1. Run all fixtures in mock mode (CI default, zero token cost)
|
|
22
|
+
npm run e2e -- --fixture all --mock --fail-on-assert
|
|
23
|
+
|
|
24
|
+
# 2. Run a specific fixture
|
|
25
|
+
npm run e2e -- --fixture login --mock
|
|
26
|
+
|
|
27
|
+
# 3. Run against an existing project in the database
|
|
28
|
+
npm run e2e -- --project <projectId> --screen <screenId>
|
|
29
|
+
|
|
30
|
+
# 4. Limit pipeline stages
|
|
31
|
+
npm run e2e -- --fixture login --mock --stages cases,data
|
|
32
|
+
|
|
33
|
+
# 5. Output JSON report for CI/monitoring
|
|
34
|
+
npm run e2e -- --fixture all --mock --json report.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Mock & Record Modes
|
|
40
|
+
|
|
41
|
+
The mock engine intercepts calls in `askTracked` and `askTrackedWithImages`.
|
|
42
|
+
|
|
43
|
+
| Environment Variable | Value | Description |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `ZETA_LLM_MOCK` | `1` or `replay` | Replays saved fixtures from `eval/fixtures/llm/`. Falls back to schema-valid deterministic mock if fixture is missing. |
|
|
46
|
+
| `ZETA_LLM_MOCK` | `record` | Hits the live LLM provider and automatically records prompt/response pairs to `eval/fixtures/llm/` for future replay. |
|
|
47
|
+
| `ZETA_LLM_MOCK_FAIL` | `429` \| `503` \| `malformed-json` \| `empty` | Injects synthetic provider failures to test error handling and retry resilience. |
|
|
48
|
+
|
|
49
|
+
> **Safety:** `ZETA_LLM_MOCK` immediately throws an exception if `NODE_ENV=production`.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Golden Set Evaluation (`eval/`)
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
eval/
|
|
57
|
+
fixtures/screens/<name>.json # Screen + elements as captured by crawler
|
|
58
|
+
fixtures/llm/<key>.json # Recorded LLM responses for deterministic replay
|
|
59
|
+
golden/<name>.golden.json # Golden assertion criteria (minCases, kinds, title pattern)
|
|
60
|
+
baseline.json # Historical score baseline
|
|
61
|
+
run-eval.ts # Evaluator runner comparing outputs against baseline
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Golden Evaluation Commands
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Run eval across all golden screens
|
|
68
|
+
npm run eval
|
|
69
|
+
|
|
70
|
+
# Accept current scores and update baseline.json
|
|
71
|
+
npm run eval:accept
|
|
72
|
+
|
|
73
|
+
# Run eval for specific screens only
|
|
74
|
+
npm run eval -- login checkout
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Golden Spec Format (`eval/golden/<name>.golden.json`)
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"screen": "login",
|
|
82
|
+
"minCases": 12,
|
|
83
|
+
"mockMinCases": 1,
|
|
84
|
+
"mustHaveKinds": ["FUNCTIONAL", "SECURITY"],
|
|
85
|
+
"kindMinimums": { "SECURITY": 2 },
|
|
86
|
+
"maxDuplicateRatio": 0.10,
|
|
87
|
+
"titlePattern": "^[^—]+ — [^—]+ — .+$",
|
|
88
|
+
"requiredCoverageTerms": ["invalid password", "empty email", "sql injection"],
|
|
89
|
+
"script": { "minAlignment": 75, "maxLintViolations": 0 }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## CI Integration
|
|
96
|
+
|
|
97
|
+
Mock evaluation is automated via GitHub Actions in [`.github/workflows/agents-eval.yml`](file:///Users/deepak/Downloads/Projects/Zeta-AI-Final/quality-intelligence-platform/.github/workflows/agents-eval.yml):
|
|
98
|
+
- Triggers on PRs modifying `packages/agents/**`, `packages/core/**`, `packages/app-brain/**`, or agent prompt seeds.
|
|
99
|
+
- Runs unit tests and `npm run e2e -- --fixture all --mock --fail-on-assert`.
|
|
100
|
+
- Uploads `report.json` as a build artifact.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Test Agent — Endpoint Contract & Behavior Testing
|
|
3
|
+
*
|
|
4
|
+
* Reads API_CONTRACT + DATA_MODEL skill files captured during crawl,
|
|
5
|
+
* generates test cases validating backend endpoint contracts:
|
|
6
|
+
* status codes, request schemas, auth, error handling — no UI required.
|
|
7
|
+
*
|
|
8
|
+
* Test kinds: API, NEGATIVE, EDGE
|
|
9
|
+
* Steps use Playwright APIRequestContext syntax.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApiTestCase {
|
|
12
|
+
title: string;
|
|
13
|
+
kind: 'API' | 'NEGATIVE' | 'EDGE';
|
|
14
|
+
priority: 'CRITICAL' | 'HIGH' | 'MEDIUM';
|
|
15
|
+
endpoint: string;
|
|
16
|
+
method: string;
|
|
17
|
+
preconditions: string;
|
|
18
|
+
steps: string[];
|
|
19
|
+
expectedResult: string;
|
|
20
|
+
groundedOn: string[];
|
|
21
|
+
tags: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function generateApiTests(opts: {
|
|
24
|
+
tenantId: string;
|
|
25
|
+
projectId: string;
|
|
26
|
+
apiEndpoints?: Array<{
|
|
27
|
+
method: string;
|
|
28
|
+
urlPattern: string;
|
|
29
|
+
}>;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
status: string;
|
|
32
|
+
cases: never[];
|
|
33
|
+
saved: null;
|
|
34
|
+
suiteName?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
status: string;
|
|
37
|
+
cases: ApiTestCase[];
|
|
38
|
+
saved: {
|
|
39
|
+
suite: {
|
|
40
|
+
tenantId: string;
|
|
41
|
+
id: string;
|
|
42
|
+
projectId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
createdAt: Date;
|
|
45
|
+
genModel: string | null;
|
|
46
|
+
category: import(".prisma/client").$Enums.SuiteCategory | null;
|
|
47
|
+
parentId: string | null;
|
|
48
|
+
ownerUserId: string | null;
|
|
49
|
+
genTotalTokens: number | null;
|
|
50
|
+
genTotalCostUsd: number | null;
|
|
51
|
+
genAt: Date | null;
|
|
52
|
+
genScreenCount: number | null;
|
|
53
|
+
coverageReport: import("@prisma/client/runtime/library").JsonValue | null;
|
|
54
|
+
};
|
|
55
|
+
testCases: {
|
|
56
|
+
tenantId: string;
|
|
57
|
+
screenId: string | null;
|
|
58
|
+
id: string;
|
|
59
|
+
projectId: string;
|
|
60
|
+
source: string;
|
|
61
|
+
status: string;
|
|
62
|
+
createdAt: Date;
|
|
63
|
+
updatedAt: Date;
|
|
64
|
+
steps: import("@prisma/client/runtime/library").JsonValue;
|
|
65
|
+
kind: string;
|
|
66
|
+
tags: import("@prisma/client/runtime/library").JsonValue;
|
|
67
|
+
approvedBy: string | null;
|
|
68
|
+
suiteId: string;
|
|
69
|
+
ref: string | null;
|
|
70
|
+
title: string;
|
|
71
|
+
priority: string;
|
|
72
|
+
preconditions: string | null;
|
|
73
|
+
expectedResult: string | null;
|
|
74
|
+
gherkin: string | null;
|
|
75
|
+
approvedAt: Date | null;
|
|
76
|
+
groundedOn: import("@prisma/client/runtime/library").JsonValue;
|
|
77
|
+
caseRefs: import("@prisma/client/runtime/library").JsonValue;
|
|
78
|
+
viewports: import("@prisma/client/runtime/library").JsonValue;
|
|
79
|
+
viewportConstraint: string;
|
|
80
|
+
techniqueOfRecord: string | null;
|
|
81
|
+
traceTo: string | null;
|
|
82
|
+
traceToExtra: import("@prisma/client/runtime/library").JsonValue;
|
|
83
|
+
testDataInline: import("@prisma/client/runtime/library").JsonValue | null;
|
|
84
|
+
dataNote: string | null;
|
|
85
|
+
scriptNote: string | null;
|
|
86
|
+
structuredSteps: import("@prisma/client/runtime/library").JsonValue | null;
|
|
87
|
+
artifactId: string | null;
|
|
88
|
+
scenarioId: string | null;
|
|
89
|
+
suspect: boolean;
|
|
90
|
+
suspectReason: string | null;
|
|
91
|
+
flakyScore: number;
|
|
92
|
+
consecutiveFlakes: number;
|
|
93
|
+
lastFlakyAt: Date | null;
|
|
94
|
+
quarantined: boolean;
|
|
95
|
+
quarantinedAt: Date | null;
|
|
96
|
+
quarantineReason: string | null;
|
|
97
|
+
genPromptTokens: number | null;
|
|
98
|
+
genCompletionTokens: number | null;
|
|
99
|
+
genCostUsd: number | null;
|
|
100
|
+
genModel: string | null;
|
|
101
|
+
dataSetId: string | null;
|
|
102
|
+
repoTargetPath: string | null;
|
|
103
|
+
stepsKey: string | null;
|
|
104
|
+
gherkinKey: string | null;
|
|
105
|
+
contentHash: string | null;
|
|
106
|
+
groundingHash: string | null;
|
|
107
|
+
locatorsJson: import("@prisma/client/runtime/library").JsonValue | null;
|
|
108
|
+
locatorsExtractedAt: Date | null;
|
|
109
|
+
captureStepScreenshots: import("@prisma/client/runtime/library").JsonValue | null;
|
|
110
|
+
}[];
|
|
111
|
+
};
|
|
112
|
+
suiteName: string;
|
|
113
|
+
}>;
|
|
114
|
+
export declare function generatePactContract(endpoints: Array<{
|
|
115
|
+
method: string;
|
|
116
|
+
path: string;
|
|
117
|
+
summary?: string;
|
|
118
|
+
operationId?: string;
|
|
119
|
+
}>, options: {
|
|
120
|
+
consumerName: string;
|
|
121
|
+
providerName: string;
|
|
122
|
+
}): Promise<string>;
|
|
123
|
+
type JsonSchema = Record<string, unknown>;
|
|
124
|
+
export declare function generateSyntheticValue(fieldName: string, schemaType?: string): string | number | boolean | null;
|
|
125
|
+
export declare function generateSyntheticBody(schema?: JsonSchema): Record<string, unknown>;
|
|
126
|
+
export {};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* API Test Agent — Endpoint Contract & Behavior Testing
|
|
4
|
+
*
|
|
5
|
+
* Reads API_CONTRACT + DATA_MODEL skill files captured during crawl,
|
|
6
|
+
* generates test cases validating backend endpoint contracts:
|
|
7
|
+
* status codes, request schemas, auth, error handling — no UI required.
|
|
8
|
+
*
|
|
9
|
+
* Test kinds: API, NEGATIVE, EDGE
|
|
10
|
+
* Steps use Playwright APIRequestContext syntax.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.generateApiTests = generateApiTests;
|
|
14
|
+
exports.generatePactContract = generatePactContract;
|
|
15
|
+
exports.generateSyntheticValue = generateSyntheticValue;
|
|
16
|
+
exports.generateSyntheticBody = generateSyntheticBody;
|
|
17
|
+
const app_brain_1 = require("@taodigital/app-brain");
|
|
18
|
+
const core_1 = require("@taodigital/core");
|
|
19
|
+
const llm_js_1 = require("./llm.js");
|
|
20
|
+
const llm_config_js_1 = require("./llm-config.js");
|
|
21
|
+
// GAP-010: helper for batched endpoint processing
|
|
22
|
+
function chunkArray(arr, size) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
25
|
+
chunks.push(arr.slice(i, i + size));
|
|
26
|
+
}
|
|
27
|
+
return chunks;
|
|
28
|
+
}
|
|
29
|
+
function parseTestCasesFromRaw(raw) {
|
|
30
|
+
const jsonMatch = raw.match(/\[[\s\S]*\]/);
|
|
31
|
+
if (!jsonMatch)
|
|
32
|
+
return [];
|
|
33
|
+
const items = JSON.parse(jsonMatch[0]);
|
|
34
|
+
return items
|
|
35
|
+
.filter((tc) => tc.title && Array.isArray(tc.steps) && tc.steps.length >= 1)
|
|
36
|
+
.map((tc) => ({
|
|
37
|
+
title: String(tc.title).slice(0, 200),
|
|
38
|
+
kind: (['API', 'NEGATIVE', 'EDGE'].includes(tc.kind) ? tc.kind : 'API'),
|
|
39
|
+
priority: (['CRITICAL', 'HIGH', 'MEDIUM'].includes(tc.priority) ? tc.priority : 'HIGH'),
|
|
40
|
+
endpoint: String(tc.endpoint || '').slice(0, 100),
|
|
41
|
+
method: String(tc.method || 'GET'),
|
|
42
|
+
preconditions: String(tc.preconditions || '').slice(0, 300),
|
|
43
|
+
steps: tc.steps.map((s) => String(s).slice(0, 400)),
|
|
44
|
+
expectedResult: String(tc.expectedResult || '').slice(0, 500),
|
|
45
|
+
groundedOn: Array.isArray(tc.groundedOn) ? tc.groundedOn.map(String) : [],
|
|
46
|
+
tags: ['api', ...(Array.isArray(tc.tags) ? tc.tags.map(String) : [])],
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
async function generateApiTests(opts) {
|
|
50
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(opts.tenantId, 'testGeneration');
|
|
51
|
+
const skillFiles = await app_brain_1.AppBrain.listSkillFiles(opts.tenantId, opts.projectId, ['API_CONTRACT', 'DATA_MODEL', 'AUTH_SESSION', 'TECH_STACK']);
|
|
52
|
+
const skillCtx = skillFiles.map((sf) => `### ${sf.kind}\n${sf.content}`).join('\n\n');
|
|
53
|
+
// GAP-010: raised default from 40 to 100; batching handles larger APIs
|
|
54
|
+
const ENDPOINT_LIMIT = parseInt(process.env.API_AGENT_ENDPOINT_LIMIT ?? '100', 10);
|
|
55
|
+
let dedupedEndpoints = [];
|
|
56
|
+
if (opts.apiEndpoints && opts.apiEndpoints.length > 0) {
|
|
57
|
+
dedupedEndpoints = [...new Map(opts.apiEndpoints.map((e) => [`${e.method}:${e.urlPattern}`, e])).values()];
|
|
58
|
+
}
|
|
59
|
+
const SYSTEM_DEFAULT = `You are an expert API QA engineer generating REST endpoint test cases.
|
|
60
|
+
Cover: status codes, request/response schema contracts, auth/authz, error handling, boundary values.
|
|
61
|
+
|
|
62
|
+
GROUNDING RULES:
|
|
63
|
+
- Only generate tests for endpoints in API_CONTRACT or the endpoint list below
|
|
64
|
+
- Never invent endpoints not documented
|
|
65
|
+
- Steps use Playwright APIRequestContext format
|
|
66
|
+
- Step format: "API [METHOD] [path]: [what is tested]"
|
|
67
|
+
- groundedOn: "endpoint:METHOD /path" pairs
|
|
68
|
+
|
|
69
|
+
OUTPUT FORMAT — valid JSON array only, no markdown fences:
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"title": "string",
|
|
73
|
+
"kind": "API|NEGATIVE|EDGE",
|
|
74
|
+
"priority": "CRITICAL|HIGH|MEDIUM",
|
|
75
|
+
"endpoint": "METHOD /path",
|
|
76
|
+
"method": "GET|POST|PUT|DELETE|PATCH",
|
|
77
|
+
"preconditions": "string",
|
|
78
|
+
"steps": ["step1", "step2"],
|
|
79
|
+
"expectedResult": "string",
|
|
80
|
+
"groundedOn": ["endpoint:METHOD /path"],
|
|
81
|
+
"tags": ["api"]
|
|
82
|
+
}
|
|
83
|
+
]`;
|
|
84
|
+
// GAP-008: CRUD chain instruction appended to every user prompt
|
|
85
|
+
const buildUserPrompt = (endpointSummary) => `Generate comprehensive API test cases for this application.
|
|
86
|
+
|
|
87
|
+
${endpointSummary ? `ENDPOINTS DISCOVERED DURING CRAWL:\n${endpointSummary}\n\n` : ''}SKILL FILE CONTEXT:
|
|
88
|
+
${skillCtx.slice(0, 6000)}
|
|
89
|
+
|
|
90
|
+
Cover these dimensions:
|
|
91
|
+
1. HAPPY PATH (API): Valid inputs → 200/201 with correct response schema
|
|
92
|
+
2. NO AUTH (NEGATIVE): Missing Bearer token → 401
|
|
93
|
+
3. WRONG ROLE (NEGATIVE): Authenticated but insufficient role → 403
|
|
94
|
+
4. MISSING FIELDS (NEGATIVE): Omit required fields → 400 with field-level errors
|
|
95
|
+
5. NOT FOUND (NEGATIVE): Valid shape, nonexistent ID → 404
|
|
96
|
+
6. INVALID TYPES (EDGE): String where number expected, null in non-nullable → 422
|
|
97
|
+
7. BOUNDARY VALUES (EDGE): Max-length strings, zero/negative numbers, empty arrays
|
|
98
|
+
8. DUPLICATE CREATE (EDGE): POST same unique resource twice → 409 Conflict
|
|
99
|
+
|
|
100
|
+
Minimum 2 test cases per major endpoint family. Reference ACTUAL endpoints.
|
|
101
|
+
|
|
102
|
+
Additionally, detect CRUD endpoint patterns: if endpoints include POST /resource AND GET /resource/:id AND PUT/PATCH /resource/:id AND DELETE /resource/:id for the same resource name, generate ONE additional test case with:
|
|
103
|
+
- kind: 'API' or 'POSITIVE'
|
|
104
|
+
- title: 'Full CRUD lifecycle for {resource}'
|
|
105
|
+
- steps: ['POST /resource to create → capture id from response.body.id', 'GET /resource/{id} to verify creation', 'PUT /resource/{id} to update', 'DELETE /resource/{id} to verify deletion']
|
|
106
|
+
- expectedResult: 'All CRUD operations succeed in sequence, id flows between steps'`;
|
|
107
|
+
const system = await (0, core_1.getAgentPrompt)('api-test-generation', undefined, opts.tenantId) ?? SYSTEM_DEFAULT;
|
|
108
|
+
let parsed = [];
|
|
109
|
+
// GAP-010: batch processing for endpoint lists exceeding ENDPOINT_LIMIT
|
|
110
|
+
if (dedupedEndpoints.length <= ENDPOINT_LIMIT) {
|
|
111
|
+
const endpointSummary = dedupedEndpoints.map((e) => `${e.method} ${e.urlPattern}`).join('\n');
|
|
112
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:api-tests:${endpointSummary.slice(0, 500)}`);
|
|
113
|
+
const t0 = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const { text: raw, usage } = await (0, llm_js_1.askTracked)(system, buildUserPrompt(endpointSummary), llmCfg, undefined, undefined, { temperature: 0, seed });
|
|
116
|
+
const latencyMs = Date.now() - t0;
|
|
117
|
+
(0, app_brain_1.recordLLMCall)({
|
|
118
|
+
tenantId: opts.tenantId, projectId: opts.projectId,
|
|
119
|
+
agentType: 'api_test', label: 'api_test_generation', usage, latencyMs,
|
|
120
|
+
}).catch(console.error);
|
|
121
|
+
parsed = parseTestCasesFromRaw(raw);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
console.error('[api-test-agent] parse error:', e?.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// Split into chunks of ENDPOINT_LIMIT, call LLM per chunk, then merge
|
|
129
|
+
const chunks = chunkArray(dedupedEndpoints, ENDPOINT_LIMIT);
|
|
130
|
+
console.log(`[api-test-agent] batching ${dedupedEndpoints.length} endpoints into ${chunks.length} chunks of ${ENDPOINT_LIMIT}`);
|
|
131
|
+
const seenKeys = new Set();
|
|
132
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
133
|
+
const chunkSummary = chunks[i].map((e) => `${e.method} ${e.urlPattern}`).join('\n');
|
|
134
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${opts.tenantId}:${opts.projectId}:api-tests:batch${i}:${chunkSummary.slice(0, 500)}`);
|
|
135
|
+
const t0 = Date.now();
|
|
136
|
+
try {
|
|
137
|
+
const { text: raw, usage } = await (0, llm_js_1.askTracked)(system, buildUserPrompt(chunkSummary), llmCfg, undefined, undefined, { temperature: 0, seed });
|
|
138
|
+
const latencyMs = Date.now() - t0;
|
|
139
|
+
(0, app_brain_1.recordLLMCall)({
|
|
140
|
+
tenantId: opts.tenantId, projectId: opts.projectId,
|
|
141
|
+
agentType: 'api_test', label: `api_test_generation_batch_${i}`, usage, latencyMs,
|
|
142
|
+
}).catch(console.error);
|
|
143
|
+
const batchCases = parseTestCasesFromRaw(raw);
|
|
144
|
+
// Deduplicate by endpoint+title across batches
|
|
145
|
+
for (const tc of batchCases) {
|
|
146
|
+
const key = `${tc.endpoint}:${tc.title}`;
|
|
147
|
+
if (!seenKeys.has(key)) {
|
|
148
|
+
seenKeys.add(key);
|
|
149
|
+
parsed.push(tc);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
console.error(`[api-test-agent] parse error in batch ${i}:`, e?.message);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (parsed.length === 0)
|
|
159
|
+
return { status: 'LLM_EMPTY', cases: [], saved: null };
|
|
160
|
+
const suiteName = 'API Contract Tests';
|
|
161
|
+
const saved = await app_brain_1.AppBrain.saveGeneratedTestCases(opts.tenantId, opts.projectId, suiteName, parsed.map((tc) => ({
|
|
162
|
+
title: tc.title, kind: tc.kind, priority: tc.priority,
|
|
163
|
+
preconditions: tc.preconditions, steps: tc.steps,
|
|
164
|
+
expectedResult: tc.expectedResult, groundedOn: tc.groundedOn,
|
|
165
|
+
tags: tc.tags, source: 'AI_GENERATED',
|
|
166
|
+
})));
|
|
167
|
+
// GAP-007: executable API test scripts
|
|
168
|
+
// After saving test cases, trigger script generation via script-agent
|
|
169
|
+
// TODO: await triggerApiTestScriptGeneration(tenantId, projectId, savedCases)
|
|
170
|
+
// This will generate Playwright request() test files as TEST_SCRIPTS skill file
|
|
171
|
+
console.log(`[api-test-agent] ${parsed.length} API test cases saved`);
|
|
172
|
+
return { status: 'GENERATED', cases: parsed, saved, suiteName };
|
|
173
|
+
}
|
|
174
|
+
// GAP-014: Pact v3 consumer contract generation
|
|
175
|
+
async function generatePactContract(endpoints, options) {
|
|
176
|
+
const interactions = endpoints
|
|
177
|
+
.filter((ep) => ep.method !== 'WEBSOCKET')
|
|
178
|
+
.map((ep) => {
|
|
179
|
+
const desc = ep.summary ?? `${ep.method} ${ep.path}`;
|
|
180
|
+
const expectedStatus = ep.method === 'POST' ? 201 : ep.method === 'DELETE' ? 204 : 200;
|
|
181
|
+
return {
|
|
182
|
+
description: desc,
|
|
183
|
+
providerState: `${ep.path} exists`,
|
|
184
|
+
request: {
|
|
185
|
+
method: ep.method.toUpperCase(),
|
|
186
|
+
path: ep.path.replace(/\{([^}]+)\}/g, (_, p) => `test-${p}`),
|
|
187
|
+
headers: { Accept: 'application/json', ...(ep.method !== 'GET' && ep.method !== 'DELETE' ? { 'Content-Type': 'application/json' } : {}) },
|
|
188
|
+
},
|
|
189
|
+
response: {
|
|
190
|
+
status: expectedStatus,
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: expectedStatus !== 204 ? { matchingRules: { body: { '': { matchers: [{ match: 'type' }] } } } } : undefined,
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
});
|
|
196
|
+
const pact = {
|
|
197
|
+
consumer: { name: options.consumerName },
|
|
198
|
+
provider: { name: options.providerName },
|
|
199
|
+
interactions,
|
|
200
|
+
metadata: { pactSpecification: { version: '3.0.0' } },
|
|
201
|
+
};
|
|
202
|
+
return JSON.stringify(pact, null, 2);
|
|
203
|
+
}
|
|
204
|
+
function generateSyntheticValue(fieldName, schemaType) {
|
|
205
|
+
const name = fieldName.toLowerCase();
|
|
206
|
+
// Name heuristics
|
|
207
|
+
if (name.includes('email'))
|
|
208
|
+
return `test.user.${Math.floor(Math.random() * 9999)}@example.com`;
|
|
209
|
+
if (name.includes('first_name') || name === 'firstname')
|
|
210
|
+
return ['Alice', 'Bob', 'Carol', 'Dave', 'Eve'][Math.floor(Math.random() * 5)];
|
|
211
|
+
if (name.includes('last_name') || name === 'lastname')
|
|
212
|
+
return ['Smith', 'Jones', 'Brown', 'Davis', 'Wilson'][Math.floor(Math.random() * 5)];
|
|
213
|
+
if (name.includes('name') && !name.includes('file'))
|
|
214
|
+
return `Test User ${Math.floor(Math.random() * 100)}`;
|
|
215
|
+
if (name.includes('phone'))
|
|
216
|
+
return `+1-555-${String(Math.floor(Math.random() * 900) + 100)}-${String(Math.floor(Math.random() * 9000) + 1000)}`;
|
|
217
|
+
if (name.includes('url') || name.includes('href') || name.includes('link'))
|
|
218
|
+
return `https://example.com/item/${Math.floor(Math.random() * 1000)}`;
|
|
219
|
+
if (name.includes('image') || name.includes('avatar') || name.includes('photo'))
|
|
220
|
+
return 'https://picsum.photos/200';
|
|
221
|
+
if (name.includes('description') || name.includes('content') || name.includes('body'))
|
|
222
|
+
return 'Sample test content for automated testing purposes.';
|
|
223
|
+
if (name.includes('title') || name.includes('subject'))
|
|
224
|
+
return `Test Item ${Math.floor(Math.random() * 1000)}`;
|
|
225
|
+
if (name.includes('id') && schemaType === 'string')
|
|
226
|
+
return `id-${Math.random().toString(36).slice(2, 10)}`;
|
|
227
|
+
if (name.includes('id') || name === 'id')
|
|
228
|
+
return Math.floor(Math.random() * 10000) + 1;
|
|
229
|
+
if (name.includes('count') || name.includes('quantity') || name.includes('amount'))
|
|
230
|
+
return Math.floor(Math.random() * 100) + 1;
|
|
231
|
+
if (name.includes('price') || name.includes('cost') || name.includes('fee'))
|
|
232
|
+
return Math.round(Math.random() * 999.99 * 100) / 100;
|
|
233
|
+
if (name.includes('date') || name.includes('created') || name.includes('updated'))
|
|
234
|
+
return new Date().toISOString();
|
|
235
|
+
if (name.includes('active') || name.includes('enabled') || name.includes('visible'))
|
|
236
|
+
return true;
|
|
237
|
+
if (name.includes('status'))
|
|
238
|
+
return 'active';
|
|
239
|
+
if (name.includes('type') || name.includes('kind') || name.includes('category'))
|
|
240
|
+
return 'standard';
|
|
241
|
+
if (name.includes('color') || name.includes('colour'))
|
|
242
|
+
return '#3b82f6';
|
|
243
|
+
if (name.includes('tags') || name.includes('labels'))
|
|
244
|
+
return ['test', 'sample'];
|
|
245
|
+
// Schema type fallbacks
|
|
246
|
+
if (schemaType === 'boolean')
|
|
247
|
+
return true;
|
|
248
|
+
if (schemaType === 'number' || schemaType === 'integer')
|
|
249
|
+
return Math.floor(Math.random() * 100) + 1;
|
|
250
|
+
if (schemaType === 'array')
|
|
251
|
+
return [];
|
|
252
|
+
if (schemaType === 'object')
|
|
253
|
+
return {};
|
|
254
|
+
return `test-${fieldName}-value`;
|
|
255
|
+
}
|
|
256
|
+
function generateSyntheticBody(schema) {
|
|
257
|
+
if (!schema || typeof schema !== 'object')
|
|
258
|
+
return {};
|
|
259
|
+
const properties = schema.properties;
|
|
260
|
+
if (!properties)
|
|
261
|
+
return {};
|
|
262
|
+
const result = {};
|
|
263
|
+
for (const [key, def] of Object.entries(properties)) {
|
|
264
|
+
result[key] = generateSyntheticValue(key, def?.type);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fires after artifact intel extraction.
|
|
3
|
+
* Reads the relevant skill file for the project, merges artifact knowledge into it,
|
|
4
|
+
* and upserts. This is how knowledge base uploads "teach" the agent swarm.
|
|
5
|
+
*/
|
|
6
|
+
export declare function learnFromArtifact(tenantId: string, projectId: string, content: string, name: string, docType: string): Promise<void>;
|
|
7
|
+
export declare function generateArtifactIntelligence(tenantId: string, projectId: string, artifactId: string, content: string, name: string, jobId?: string): Promise<{
|
|
8
|
+
summary: string;
|
|
9
|
+
keywords: string[];
|
|
10
|
+
docType: string;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.learnFromArtifact = learnFromArtifact;
|
|
4
|
+
exports.generateArtifactIntelligence = generateArtifactIntelligence;
|
|
5
|
+
const llm_js_1 = require("./llm.js");
|
|
6
|
+
const llm_config_js_1 = require("./llm-config.js");
|
|
7
|
+
const app_brain_1 = require("@taodigital/app-brain");
|
|
8
|
+
const core_1 = require("@taodigital/core");
|
|
9
|
+
const SYSTEM = `You are a document intelligence analyst. Given the markdown content of an uploaded document, extract:
|
|
10
|
+
1. A concise summary (2-3 sentences) describing what this document is about and its purpose.
|
|
11
|
+
2. 8-15 search keywords that best represent the document's topics, entities, and domain.
|
|
12
|
+
3. The document type classification.
|
|
13
|
+
|
|
14
|
+
Respond with ONLY valid JSON (no markdown, no code blocks):
|
|
15
|
+
{
|
|
16
|
+
"summary": "...",
|
|
17
|
+
"keywords": ["keyword1", "keyword2", ...],
|
|
18
|
+
"docType": "one of: REQUIREMENTS|TEST_CASES|API_SPEC|USER_STORY|ARCHITECTURE|SPREADSHEET|TEST_DATA|DATASHEET|REPORT|OTHER"
|
|
19
|
+
}`;
|
|
20
|
+
// Maps artifact docType → which skill file to update
|
|
21
|
+
const DOC_TYPE_SKILL_MAP = {
|
|
22
|
+
TEST_CASES: 'COVERAGE_MATRIX',
|
|
23
|
+
SPREADSHEET: 'COVERAGE_MATRIX', // Excel/CSV test files
|
|
24
|
+
DATASHEET: 'COVERAGE_MATRIX', // Datasheets carry test data context
|
|
25
|
+
TEST_DATA: 'COVERAGE_MATRIX', // Test data files feed into coverage context
|
|
26
|
+
REQUIREMENTS: 'BUSINESS_DOMAIN',
|
|
27
|
+
USER_STORY: 'BUSINESS_DOMAIN',
|
|
28
|
+
REPORT: 'BUSINESS_DOMAIN', // Reports carry domain/business knowledge
|
|
29
|
+
API_SPEC: 'API_CONTRACT',
|
|
30
|
+
ARCHITECTURE: 'TECH_STACK',
|
|
31
|
+
};
|
|
32
|
+
const MERGE_PROMPTS = {
|
|
33
|
+
COVERAGE_MATRIX: `You are a QA knowledge integrator. Given an existing COVERAGE_MATRIX skill file and a new test case document, merge new information in.
|
|
34
|
+
Extract test scenario names, covered features, test types (FUNCTIONAL/SECURITY/ACCESSIBILITY/PERFORMANCE), and any gaps.
|
|
35
|
+
Return a complete updated COVERAGE_MATRIX skill file in Markdown. Keep existing data, add new rows/sections.`,
|
|
36
|
+
BUSINESS_DOMAIN: `You are a domain knowledge integrator. Given an existing BUSINESS_DOMAIN skill file and a requirements/user-story document, merge new domain knowledge.
|
|
37
|
+
Extract user roles, business rules, user journeys, domain entities, and constraints.
|
|
38
|
+
Return a complete updated BUSINESS_DOMAIN skill file in Markdown. Keep existing data, add new sections.`,
|
|
39
|
+
API_CONTRACT: `You are an API knowledge integrator. Given an existing API_CONTRACT skill file and a new API spec, merge new endpoint/contract information.
|
|
40
|
+
Extract endpoints, auth patterns, request/response shapes, and error codes.
|
|
41
|
+
Return a complete updated API_CONTRACT skill file in Markdown. Keep existing data, add new endpoints.`,
|
|
42
|
+
TECH_STACK: `You are a tech stack integrator. Given an existing TECH_STACK skill file and a new architecture document, merge new technical information.
|
|
43
|
+
Extract frameworks, libraries, deployment topology, and technical constraints.
|
|
44
|
+
Return a complete updated TECH_STACK skill file in Markdown. Keep existing data, add new details.`,
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Fires after artifact intel extraction.
|
|
48
|
+
* Reads the relevant skill file for the project, merges artifact knowledge into it,
|
|
49
|
+
* and upserts. This is how knowledge base uploads "teach" the agent swarm.
|
|
50
|
+
*/
|
|
51
|
+
async function learnFromArtifact(tenantId, projectId, content, name, docType) {
|
|
52
|
+
const skillKind = DOC_TYPE_SKILL_MAP[docType];
|
|
53
|
+
if (!skillKind)
|
|
54
|
+
return;
|
|
55
|
+
const promptDefault = MERGE_PROMPTS[skillKind];
|
|
56
|
+
if (!promptDefault)
|
|
57
|
+
return;
|
|
58
|
+
const prompt = await (0, core_1.getAgentPrompt)('artifact-intel', skillKind, tenantId) ?? promptDefault;
|
|
59
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'appBrain').catch(() => null);
|
|
60
|
+
if (!llmCfg)
|
|
61
|
+
return;
|
|
62
|
+
const existing = await app_brain_1.AppBrain.getSkillFile(tenantId, projectId, skillKind);
|
|
63
|
+
const existingContent = existing?.content ?? `# ${skillKind}\n\n(No data yet — first artifact)`;
|
|
64
|
+
const user = `EXISTING ${skillKind} SKILL FILE:\n${existingContent.slice(0, 4000)}\n\n---\n\nNEW DOCUMENT "${name}":\n${content.slice(0, 6000)}`;
|
|
65
|
+
const t0 = Date.now();
|
|
66
|
+
const contentKey = (existingContent ?? '').slice(0, 500);
|
|
67
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:${projectId}:merge-skill:${skillKind}:${name}:${contentKey}`);
|
|
68
|
+
const result = await (0, llm_js_1.askTracked)(prompt, user, llmCfg, 2000, undefined, { temperature: 0, seed });
|
|
69
|
+
(0, app_brain_1.recordLLMCall)({
|
|
70
|
+
tenantId,
|
|
71
|
+
projectId,
|
|
72
|
+
agentType: 'artifact_learn',
|
|
73
|
+
label: `${skillKind} ← ${name}`,
|
|
74
|
+
usage: result.usage,
|
|
75
|
+
latencyMs: Date.now() - t0,
|
|
76
|
+
}).catch(console.error);
|
|
77
|
+
const updatedContent = result.text.trim();
|
|
78
|
+
if (updatedContent.length >= 200) {
|
|
79
|
+
await app_brain_1.AppBrain.upsertSkillFile(tenantId, projectId, skillKind, updatedContent, {
|
|
80
|
+
summary: `Learned from artifact: ${name}`,
|
|
81
|
+
tags: ['artifact-learned', docType.toLowerCase()],
|
|
82
|
+
source: 'ARTIFACT',
|
|
83
|
+
});
|
|
84
|
+
console.log(`[artifact-learn] Updated ${skillKind} from "${name}"`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function generateArtifactIntelligence(tenantId, projectId, artifactId, content, name, jobId) {
|
|
88
|
+
const t0 = Date.now();
|
|
89
|
+
let parsed = {};
|
|
90
|
+
let llmSucceeded = false;
|
|
91
|
+
try {
|
|
92
|
+
const llmCfg = await (0, llm_config_js_1.getTaskAgentLLMConfig)(tenantId, 'appBrain');
|
|
93
|
+
// Truncate to avoid large token usage — first 8000 chars is enough for summary/keywords
|
|
94
|
+
const excerpt = content.length > 8000 ? content.slice(0, 8000) + '\n… (truncated)' : content;
|
|
95
|
+
const user = `Document name: ${name}\n\nContent:\n${excerpt}`;
|
|
96
|
+
const seed = (0, llm_js_1.deterministicSeed)(`${tenantId}:${projectId}:artifact-intel:${artifactId}`);
|
|
97
|
+
const effectiveSystem = await (0, core_1.getAgentPrompt)('artifact-intel', undefined, tenantId) ?? SYSTEM;
|
|
98
|
+
const result = await (0, llm_js_1.askTracked)(effectiveSystem, user, llmCfg, 512, undefined, { temperature: 0, seed });
|
|
99
|
+
(0, app_brain_1.recordLLMCall)({
|
|
100
|
+
tenantId,
|
|
101
|
+
projectId,
|
|
102
|
+
agentType: 'artifact_intel',
|
|
103
|
+
label: name,
|
|
104
|
+
jobId,
|
|
105
|
+
usage: result.usage,
|
|
106
|
+
latencyMs: Date.now() - t0,
|
|
107
|
+
}).catch(console.error);
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(result.text);
|
|
110
|
+
llmSucceeded = true;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
const m = result.text.match(/\{[\s\S]*\}/);
|
|
114
|
+
if (m) {
|
|
115
|
+
try {
|
|
116
|
+
parsed = JSON.parse(m[0]);
|
|
117
|
+
llmSucceeded = true;
|
|
118
|
+
}
|
|
119
|
+
catch { /* ignore */ }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
// Any failure (no LLM config, network error, etc.) — throw so the caller's
|
|
125
|
+
// trackArtifactIntel marks the job as failed and summary stays null in the DB.
|
|
126
|
+
// A null summary keeps processingIntel=true on the UI, showing the spinner.
|
|
127
|
+
console.error('[artifact-intel] failed for', name, e?.message);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
// Validate that the LLM returned a usable summary — if parse succeeded but the
|
|
131
|
+
// response had no summary field, throw so the DB is NOT written and the artifact
|
|
132
|
+
// stays in "Analysing…" state on the UI (processingIntel = !a.summary = true).
|
|
133
|
+
const summary = typeof parsed.summary === 'string' && parsed.summary.trim().length > 0
|
|
134
|
+
? parsed.summary.trim()
|
|
135
|
+
: null;
|
|
136
|
+
const keywords = Array.isArray(parsed.keywords) ? parsed.keywords.filter((k) => typeof k === 'string') : [];
|
|
137
|
+
const docType = parsed.docType ?? 'OTHER';
|
|
138
|
+
if (!llmSucceeded || !summary) {
|
|
139
|
+
const reason = !llmSucceeded ? 'JSON parse failed' : 'no summary in response';
|
|
140
|
+
throw new Error(`[artifact-intel] LLM analysis incomplete for "${name}" (${reason}) — summary stays null so UI shows spinner`);
|
|
141
|
+
}
|
|
142
|
+
// Persist intelligence to DB — only reached when LLM succeeded with a real summary.
|
|
143
|
+
// This ensures processingIntel (= !a.summary) correctly resolves to false only on success.
|
|
144
|
+
await app_brain_1.AppBrain.updateArtifactIntelligence(tenantId, artifactId, { summary, keywords, docType, chunkCount: 1 });
|
|
145
|
+
// Re-index with keywords prepended for better BM25 recall
|
|
146
|
+
await app_brain_1.AppBrain.enrichChunkKeywords(tenantId, projectId, artifactId, content, name, keywords);
|
|
147
|
+
// Fire-and-forget: merge artifact knowledge into the relevant skill file (swarm learning)
|
|
148
|
+
if (DOC_TYPE_SKILL_MAP[docType]) {
|
|
149
|
+
learnFromArtifact(tenantId, projectId, content, name, docType).catch((e) => console.error('[artifact-learn] failed for', name, e?.message));
|
|
150
|
+
}
|
|
151
|
+
return { summary, keywords, docType };
|
|
152
|
+
}
|