@miniidealab/openlogos 0.5.0 → 0.5.3

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.
@@ -0,0 +1,147 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://openlogos.ai/schemas/logos.config.json",
4
+ "title": "OpenLogos Project Configuration",
5
+ "description": "logos.config.json 是 OpenLogos 项目的核心配置文件(位于 logos/ 目录下),定义项目的基本信息和文档模块结构。",
6
+ "type": "object",
7
+ "required": ["name", "documents"],
8
+ "properties": {
9
+ "name": {
10
+ "type": "string",
11
+ "description": "项目名称"
12
+ },
13
+ "description": {
14
+ "type": "string",
15
+ "description": "项目描述"
16
+ },
17
+ "maxOpenTabs": {
18
+ "type": "integer",
19
+ "minimum": 1,
20
+ "default": 20,
21
+ "description": "RunLogos 中最大打开标签页数(仅 RunLogos 使用)"
22
+ },
23
+ "modules": {
24
+ "type": "array",
25
+ "description": "代码模块定义(保留字段,供 RunLogos 使用)",
26
+ "items": {
27
+ "type": "object"
28
+ }
29
+ },
30
+ "fileModules": {
31
+ "type": "object",
32
+ "description": "文件级模块映射(保留字段,供 RunLogos 使用)"
33
+ },
34
+ "locale": {
35
+ "type": "string",
36
+ "enum": ["en", "zh"],
37
+ "default": "en",
38
+ "description": "CLI and RunLogos display language"
39
+ },
40
+ "documents": {
41
+ "type": "object",
42
+ "description": "文档模块定义。每个 key 是模块标识符,value 定义模块的标签、路径和文件匹配模式。",
43
+ "additionalProperties": {
44
+ "$ref": "#/definitions/DocumentModule"
45
+ }
46
+ },
47
+ "verify": {
48
+ "type": "object",
49
+ "description": "测试验收配置(可选)。配置 openlogos verify 的行为。",
50
+ "properties": {
51
+ "result_path": {
52
+ "type": "string",
53
+ "default": "logos/resources/verify/test-results.jsonl",
54
+ "description": "测试结果 JSONL 文件路径(相对项目根目录)"
55
+ },
56
+ "test_command": {
57
+ "type": "string",
58
+ "description": "测试命令(可选)。配置后 openlogos verify 会先执行此命令再读取结果"
59
+ }
60
+ }
61
+ }
62
+ },
63
+ "definitions": {
64
+ "DocumentModule": {
65
+ "type": "object",
66
+ "required": ["label", "path", "pattern"],
67
+ "properties": {
68
+ "label": {
69
+ "$ref": "#/definitions/I18nLabel",
70
+ "description": "模块显示名称(支持多语言)"
71
+ },
72
+ "path": {
73
+ "type": "string",
74
+ "description": "模块根目录的相对路径(相对于 logos.config.json 所在目录)"
75
+ },
76
+ "pattern": {
77
+ "type": "string",
78
+ "description": "文件匹配的 glob 模式"
79
+ }
80
+ }
81
+ },
82
+ "I18nLabel": {
83
+ "type": "object",
84
+ "required": ["en"],
85
+ "properties": {
86
+ "en": {
87
+ "type": "string",
88
+ "description": "英文标签"
89
+ },
90
+ "zh": {
91
+ "type": "string",
92
+ "description": "中文标签"
93
+ }
94
+ },
95
+ "additionalProperties": {
96
+ "type": "string"
97
+ }
98
+ }
99
+ },
100
+ "examples": [
101
+ {
102
+ "name": "My SaaS Project",
103
+ "description": "A SaaS product following OpenLogos methodology",
104
+ "locale": "en",
105
+ "documents": {
106
+ "prd": {
107
+ "label": { "en": "Product Docs", "zh": "产品文档" },
108
+ "path": "./resources/prd",
109
+ "pattern": "**/*.{md,html,htm,pdf}"
110
+ },
111
+ "api": {
112
+ "label": { "en": "API Docs", "zh": "API 文档" },
113
+ "path": "./resources/api",
114
+ "pattern": "**/*.{yaml,yml,json}"
115
+ },
116
+ "test": {
117
+ "label": { "en": "Test Cases", "zh": "测试用例" },
118
+ "path": "./resources/test",
119
+ "pattern": "**/*.md"
120
+ },
121
+ "scenario": {
122
+ "label": { "en": "Scenarios", "zh": "业务场景" },
123
+ "path": "./resources/scenario",
124
+ "pattern": "**/*.json"
125
+ },
126
+ "database": {
127
+ "label": { "en": "Database", "zh": "数据库" },
128
+ "path": "./resources/database",
129
+ "pattern": "**/*.sql"
130
+ },
131
+ "verify": {
132
+ "label": { "en": "Verify Reports", "zh": "验收报告" },
133
+ "path": "./resources/verify",
134
+ "pattern": "**/*.{jsonl,md}"
135
+ },
136
+ "changes": {
137
+ "label": { "en": "Change Proposals", "zh": "变更提案" },
138
+ "path": "./changes",
139
+ "pattern": "**/*.{md,json}"
140
+ }
141
+ },
142
+ "verify": {
143
+ "result_path": "logos/resources/verify/test-results.jsonl"
144
+ }
145
+ }
146
+ ]
147
+ }
@@ -0,0 +1,225 @@
1
+ # SQLite 结构化注释规范
2
+
3
+ > 版本:0.1.0
4
+ >
5
+ > 本文档定义 OpenLogos 项目中 SQLite DDL 的结构化注释格式。AI 在生成 SQLite `schema.sql` 时必须遵循此格式输出表注释和字段注释,使其可被 `parseSqlComments()` 等工具链解析。
6
+
7
+ ## 概述
8
+
9
+ PostgreSQL 和 MySQL 提供原生的注释语法(`COMMENT ON` / `COMMENT`),但 SQLite 不支持任何形式的元数据注释。OpenLogos 定义了一套基于 SQL 行注释(`--`)的结构化约定,让 SQLite DDL 具备等价的元数据表达能力:
10
+
11
+ - **`-- @comment`**:字段注释,放在字段定义行的紧邻上方
12
+ - **`-- @table-comment`**:表注释,放在 `CREATE TABLE` 语句的紧邻下方
13
+
14
+ 这套约定对标准 SQL 工具完全透明(只是普通注释),不影响 SQLite 执行。
15
+
16
+ ## 适用范围
17
+
18
+ 仅当 `logos-project.yaml` 的 `tech_stack.database` 为 **SQLite** 时激活此约定。PostgreSQL 和 MySQL 项目继续使用各自的原生注释语法。
19
+
20
+ ## 字段注释:`-- @comment`
21
+
22
+ ### 语法
23
+
24
+ ```sql
25
+ -- @comment <描述文本>
26
+ <字段定义行>
27
+ ```
28
+
29
+ ### 规则
30
+
31
+ 1. `-- @comment` 行必须**紧邻**目标字段定义行的上方
32
+ 2. `-- @comment` 与字段定义之间**不允许空行**(空行会断开关联)
33
+ 3. 多行注释:连续多个 `-- @comment` 行会自动拼接为一条注释(以空格连接)
34
+ 4. 约束行(`FOREIGN KEY`、独立 `CHECK`、独立 `UNIQUE`)**不需要也不消费** `-- @comment`
35
+
36
+ ### 示例
37
+
38
+ 单行注释:
39
+
40
+ ```sql
41
+ -- @comment 用户唯一标识,UUID v4 字符串
42
+ id TEXT PRIMARY KEY NOT NULL,
43
+ ```
44
+
45
+ 多行注释(自动拼接):
46
+
47
+ ```sql
48
+ -- @comment 账户余额,单位:分 [USD cents]
49
+ -- @comment 禁止使用 DECIMAL/FLOAT 存储金额
50
+ balance INTEGER NOT NULL DEFAULT 0,
51
+ ```
52
+
53
+ 解析结果:`"账户余额,单位:分 [USD cents] 禁止使用 DECIMAL/FLOAT 存储金额"`
54
+
55
+ ### 不需要注释的行
56
+
57
+ 以下行不应添加 `-- @comment`,解析器也不会将 `-- @comment` 关联到它们:
58
+
59
+ ```sql
60
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
61
+ ```
62
+
63
+ ## 表注释:`-- @table-comment`
64
+
65
+ ### 语法
66
+
67
+ ```sql
68
+ );
69
+ -- @table-comment <表名> <描述文本>
70
+ ```
71
+
72
+ ### 规则
73
+
74
+ 1. `-- @table-comment` 放在 `CREATE TABLE ... ();` 语句的**紧邻下方**
75
+ 2. `<表名>` 必须与 `CREATE TABLE` 中的表名完全一致
76
+ 3. `<表名>` 与 `<描述文本>` 之间用空格分隔,第一个空格之后的所有内容为描述
77
+
78
+ ### 示例
79
+
80
+ ```sql
81
+ CREATE TABLE users (
82
+ -- @comment 用户唯一标识
83
+ id TEXT PRIMARY KEY NOT NULL,
84
+ -- @comment 用户邮箱
85
+ email TEXT NOT NULL UNIQUE
86
+ );
87
+ -- @table-comment users 用户基础信息表,存储核心用户数据
88
+ ```
89
+
90
+ ## 完整示例
91
+
92
+ ```sql
93
+ -- TaskFlow v0.1 — SQLite DDL
94
+ -- 方言:SQLite 3;连接须开启 PRAGMA foreign_keys = ON;
95
+ -- 生成日期:2026-04-07
96
+
97
+ -- ---------------------------------------------------------------------------
98
+ -- users(来源:auth.yaml → register, login)
99
+ -- ---------------------------------------------------------------------------
100
+ CREATE TABLE users (
101
+ -- @comment 用户唯一标识,UUID v4 字符串
102
+ id TEXT PRIMARY KEY NOT NULL,
103
+ -- @comment 用户邮箱,已归一化为小写
104
+ email TEXT NOT NULL UNIQUE,
105
+ -- @comment Argon2id 密码哈希,仅存哈希
106
+ password_hash TEXT NOT NULL,
107
+ -- @comment 创建时间,ISO 8601 格式
108
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
109
+ -- @comment 最后更新时间
110
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
111
+ );
112
+ -- @table-comment users 用户表,存储注册用户的核心信息
113
+
114
+ CREATE INDEX idx_users_email ON users (email);
115
+
116
+ -- ---------------------------------------------------------------------------
117
+ -- tasks(来源:tasks.yaml → create, list, get, update, delete)
118
+ -- ---------------------------------------------------------------------------
119
+ CREATE TABLE tasks (
120
+ -- @comment 任务唯一标识
121
+ id TEXT PRIMARY KEY NOT NULL,
122
+ -- @comment 任务归属用户 ID
123
+ user_id TEXT NOT NULL,
124
+ -- @comment 任务标题
125
+ title TEXT NOT NULL,
126
+ -- @comment 任务详细描述,可为空
127
+ description TEXT,
128
+ -- @comment 任务状态:todo / in_progress / done
129
+ status TEXT NOT NULL DEFAULT 'todo' CHECK (status IN ('todo', 'in_progress', 'done')),
130
+ -- @comment 任务优先级:low / medium / high
131
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high')),
132
+ -- @comment 创建时间
133
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
134
+ -- @comment 最后更新时间
135
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
136
+ FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
137
+ );
138
+ -- @table-comment tasks 任务表,存储用户创建的所有任务
139
+
140
+ CREATE INDEX idx_tasks_user_status ON tasks (user_id, status);
141
+ CREATE INDEX idx_tasks_user_updated ON tasks (user_id, updated_at);
142
+ ```
143
+
144
+ ## 解析算法
145
+
146
+ 解析器逐行扫描 SQL 文件,维护以下状态:
147
+
148
+ - `currentTable: string | null` — 当前所在的 `CREATE TABLE` 上下文
149
+ - `pendingComment: string[]` — 待关联的注释缓冲区
150
+
151
+ ### 伪代码
152
+
153
+ ```
154
+ for each line in file:
155
+ trimmed = line.trim()
156
+
157
+ if trimmed starts with "-- @table-comment ":
158
+ extract tableName and description
159
+ associate description with tableName
160
+ continue
161
+
162
+ if trimmed starts with "-- @comment ":
163
+ extract text after "-- @comment "
164
+ append to pendingComment
165
+ continue
166
+
167
+ if trimmed is empty:
168
+ clear pendingComment
169
+ continue
170
+
171
+ if trimmed matches /^CREATE TABLE\s+(\w+)/i:
172
+ set currentTable to captured name
173
+ clear pendingComment (table-level comment uses @table-comment)
174
+ continue
175
+
176
+ if currentTable is set AND trimmed is a column definition:
177
+ extract columnName (first word-like token)
178
+ if pendingComment is not empty:
179
+ associate joined pendingComment with currentTable.columnName
180
+ clear pendingComment
181
+ add column to currentTable's column list
182
+
183
+ if trimmed starts with ")":
184
+ clear currentTable
185
+ clear pendingComment
186
+ ```
187
+
188
+ ### 列定义识别
189
+
190
+ 一行被视为列定义,当且仅当:
191
+ 1. 当前在 `CREATE TABLE` 块内(`currentTable` 不为 `null`)
192
+ 2. 该行**不是**以 `FOREIGN KEY`、`CHECK`、`UNIQUE`、`PRIMARY KEY`(独立约束)、`CONSTRAINT` 开头(不区分大小写)
193
+ 3. 该行**不是** `)`(表结束行)
194
+ 4. 该行**不是** `--` 开头的注释行
195
+
196
+ 列名提取:取该行第一个匹配 `/^\s*["']?(\w+)["']?/` 的标识符。
197
+
198
+ ## 输出数据结构
199
+
200
+ ```typescript
201
+ interface SchemaMetadata {
202
+ tables: TableMeta[];
203
+ }
204
+
205
+ interface TableMeta {
206
+ name: string;
207
+ comment?: string;
208
+ columns: ColumnMeta[];
209
+ }
210
+
211
+ interface ColumnMeta {
212
+ name: string;
213
+ comment?: string;
214
+ }
215
+ ```
216
+
217
+ ## 与其他方言的关系
218
+
219
+ | 数据库 | 表注释 | 字段注释 |
220
+ |--------|--------|---------|
221
+ | PostgreSQL | `COMMENT ON TABLE t IS '...'` | `COMMENT ON COLUMN t.c IS '...'` |
222
+ | MySQL | `CREATE TABLE t (...) COMMENT = '...'` | `col TYPE COMMENT '...'` |
223
+ | **SQLite** | **`-- @table-comment t 描述`** | **`-- @comment 描述`** |
224
+
225
+ OpenLogos 的 `db-designer` Skill 会根据 `tech_stack.database` 自动选择正确的注释方式。工具链中的 `parseSqlComments()` 函数专门解析 SQLite 的 `@comment` / `@table-comment` 标记。
@@ -0,0 +1,255 @@
1
+ # 测试结果格式规范
2
+
3
+ > 版本:0.1.0
4
+ >
5
+ > 本文档定义 OpenLogos 的标准化测试结果输出格式。AI 在生成测试代码时必须内嵌 reporter,按此格式输出每个用例的运行结果,供 `openlogos verify` 读取和验收。
6
+
7
+ ## 概述
8
+
9
+ OpenLogos 不绑定任何测试框架。取而代之的做法是:**AI 在生成测试代码时内嵌一个小型 reporter**,测试运行后自动将每个用例的结果追加写入统一格式的文件。`openlogos verify` 命令只需读取该文件即可完成验收判定。
10
+
11
+ 这套机制的核心优势:
12
+
13
+ - **零框架依赖**:vitest、jest、pytest、go test、cargo test 均可产出同一格式
14
+ - **零适配成本**:`openlogos verify` 只解析一种格式
15
+ - **AI 天然能做**:reporter 代码不超过 20 行,AI 在生成测试代码时顺手写入
16
+ - **用例 ID 是原生的**:不需要从测试名称里正则提取,ID 直接是数据字段
17
+
18
+ ## 文件路径
19
+
20
+ 测试结果文件的默认路径为:
21
+
22
+ ```
23
+ logos/resources/verify/test-results.jsonl
24
+ ```
25
+
26
+ 可通过 `logos.config.json` 的 `verify.result_path` 字段自定义路径。
27
+
28
+ ## 格式:JSONL
29
+
30
+ 文件格式为 **JSONL**(JSON Lines)——每行一个独立的 JSON 对象,以换行符分隔。
31
+
32
+ 选择 JSONL 而非完整 JSON 数组的理由:
33
+
34
+ | 特性 | JSONL | JSON 数组 |
35
+ |------|-------|----------|
36
+ | 追加写入 | 直接 append 一行 | 需维护数组闭合括号 |
37
+ | 流式读取 | 逐行解析 | 需读完整个文件 |
38
+ | 部分损坏 | 一行损坏不影响其他行 | 括号不匹配则整个文件不可解析 |
39
+ | 跨语言写入 | `JSON.stringify(obj) + "\n"` | 需手动管理逗号和括号 |
40
+
41
+ ## 字段定义
42
+
43
+ 每行是一个 JSON 对象,包含以下字段:
44
+
45
+ | 字段 | 类型 | 必需 | 说明 |
46
+ |------|------|------|------|
47
+ | `id` | string | 是 | 用例 ID,与 `test-cases.md` 中的 `UT-xx` / `ST-xx` 完全一致 |
48
+ | `status` | `"pass"` \| `"fail"` \| `"skip"` | 是 | 运行结果 |
49
+ | `duration_ms` | number | 否 | 执行耗时(毫秒) |
50
+ | `timestamp` | string (ISO 8601) | 否 | 执行时间,如 `2026-04-03T15:30:01Z` |
51
+ | `error` | string | `status=fail` 时必需 | 失败原因(断言错误信息) |
52
+ | `scenario` | string | 否 | 场景编号(如 `S01`),用于验证 ID 前缀一致性 |
53
+
54
+ ### 示例
55
+
56
+ ```jsonl
57
+ {"id":"UT-S01-01","status":"pass","duration_ms":12,"timestamp":"2026-04-03T15:30:01Z"}
58
+ {"id":"UT-S01-02","status":"fail","duration_ms":45,"timestamp":"2026-04-03T15:30:01Z","error":"Expected exit code 0, got 1"}
59
+ {"id":"UT-S01-03","status":"skip","timestamp":"2026-04-03T15:30:01Z"}
60
+ {"id":"ST-S01-01","status":"pass","duration_ms":230,"timestamp":"2026-04-03T15:30:02Z","scenario":"S01"}
61
+ ```
62
+
63
+ ### 字段约束
64
+
65
+ - `id` 必须匹配正则 `^(UT|ST)-S\d{2}-\d{2,3}$`
66
+ - `status` 仅允许三个值:`pass`、`fail`、`skip`
67
+ - `error` 在 `status=fail` 时必须提供,其他状态可省略
68
+ - 同一个 `id` 如果出现多次(如重试),`openlogos verify` 取**最后一次**的结果
69
+
70
+ ## 运行约定
71
+
72
+ ### 清空策略
73
+
74
+ 每次完整测试运行前,reporter 应**清空**(truncate)结果文件,确保文件只包含最近一次运行的结果。推荐方式:
75
+
76
+ - 在测试套件的 `globalSetup` 或等效钩子中清空文件
77
+ - 或者 reporter 的初始化阶段写入空文件
78
+
79
+ ### 目录创建
80
+
81
+ reporter 在写入前应确保 `logos/resources/verify/` 目录存在(`mkdir -p` 等效操作)。
82
+
83
+ ## AI 生成 reporter 代码模板
84
+
85
+ 以下是各语言的 reporter 参考实现。AI 在 Phase 3 Step 4(代码生成)时,应根据项目的 `tech_stack` 选择对应语言的模板,嵌入到测试代码中。
86
+
87
+ ### TypeScript (vitest / jest)
88
+
89
+ ````typescript
90
+ import { appendFileSync, writeFileSync, mkdirSync } from 'node:fs';
91
+ import { dirname } from 'node:path';
92
+
93
+ const RESULT_PATH = 'logos/resources/verify/test-results.jsonl';
94
+ let initialized = false;
95
+
96
+ function reportResult(
97
+ id: string,
98
+ status: 'pass' | 'fail' | 'skip',
99
+ error?: string,
100
+ durationMs?: number,
101
+ ) {
102
+ if (!initialized) {
103
+ mkdirSync(dirname(RESULT_PATH), { recursive: true });
104
+ writeFileSync(RESULT_PATH, '');
105
+ initialized = true;
106
+ }
107
+ const record: Record<string, unknown> = {
108
+ id,
109
+ status,
110
+ timestamp: new Date().toISOString(),
111
+ };
112
+ if (durationMs !== undefined) record.duration_ms = durationMs;
113
+ if (error) record.error = error;
114
+ appendFileSync(RESULT_PATH, JSON.stringify(record) + '\n');
115
+ }
116
+ ````
117
+
118
+ 在测试用例中使用:
119
+
120
+ ````typescript
121
+ import { describe, it, expect } from 'vitest';
122
+
123
+ describe('S01: CLI Init', () => {
124
+ it('UT-S01-01: should detect project name from package.json', () => {
125
+ const start = Date.now();
126
+ try {
127
+ const result = detectProjectName('/path/to/project');
128
+ expect(result.name).toBe('my-project');
129
+ reportResult('UT-S01-01', 'pass', undefined, Date.now() - start);
130
+ } catch (e) {
131
+ reportResult('UT-S01-01', 'fail', String(e), Date.now() - start);
132
+ throw e;
133
+ }
134
+ });
135
+ });
136
+ ````
137
+
138
+ ### Python (pytest)
139
+
140
+ ````python
141
+ # conftest.py
142
+ import json
143
+ import os
144
+ import time
145
+ import re
146
+ import pytest
147
+
148
+ RESULT_PATH = "logos/resources/verify/test-results.jsonl"
149
+ _initialized = False
150
+
151
+
152
+ def _ensure_file():
153
+ global _initialized
154
+ if not _initialized:
155
+ os.makedirs(os.path.dirname(RESULT_PATH), exist_ok=True)
156
+ open(RESULT_PATH, "w").close()
157
+ _initialized = True
158
+
159
+
160
+ def _extract_test_id(nodeid: str) -> str | None:
161
+ """Extract UT-S01-01 or ST-S01-01 from test function name."""
162
+ match = re.search(r"(UT|ST)_S\d{2}_\d{2,3}", nodeid)
163
+ if match:
164
+ return match.group().replace("_", "-")
165
+ return None
166
+
167
+
168
+ @pytest.hookimpl(tryfirst=True, hookwrapper=True)
169
+ def pytest_runtest_makereport(item, call):
170
+ outcome = yield
171
+ report = outcome.get_result()
172
+ if report.when == "call":
173
+ _ensure_file()
174
+ test_id = _extract_test_id(item.nodeid)
175
+ if not test_id:
176
+ return
177
+ record = {
178
+ "id": test_id,
179
+ "status": "pass" if report.passed else "fail",
180
+ "duration_ms": round(report.duration * 1000),
181
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
182
+ }
183
+ if report.failed:
184
+ record["error"] = str(report.longrepr)[:500]
185
+ with open(RESULT_PATH, "a") as f:
186
+ f.write(json.dumps(record) + "\n")
187
+ ````
188
+
189
+ Python 测试函数命名约定——用下划线替代连字符:
190
+
191
+ ````python
192
+ def test_UT_S01_01_detect_project_name():
193
+ result = detect_project_name("/path/to/project")
194
+ assert result["name"] == "my-project"
195
+ ````
196
+
197
+ ### Go
198
+
199
+ ````go
200
+ package testutil
201
+
202
+ import (
203
+ "encoding/json"
204
+ "fmt"
205
+ "os"
206
+ "path/filepath"
207
+ "sync"
208
+ "time"
209
+ )
210
+
211
+ const ResultPath = "logos/resources/verify/test-results.jsonl"
212
+
213
+ var (
214
+ once sync.Once
215
+ mu sync.Mutex
216
+ )
217
+
218
+ type TestResult struct {
219
+ ID string `json:"id"`
220
+ Status string `json:"status"`
221
+ DurationMs int64 `json:"duration_ms,omitempty"`
222
+ Timestamp string `json:"timestamp"`
223
+ Error string `json:"error,omitempty"`
224
+ }
225
+
226
+ func ReportResult(id, status string, durationMs int64, err string) {
227
+ once.Do(func() {
228
+ os.MkdirAll(filepath.Dir(ResultPath), 0o755)
229
+ os.WriteFile(ResultPath, nil, 0o644)
230
+ })
231
+ r := TestResult{
232
+ ID: id,
233
+ Status: status,
234
+ DurationMs: durationMs,
235
+ Timestamp: time.Now().UTC().Format(time.RFC3339),
236
+ Error: err,
237
+ }
238
+ b, _ := json.Marshal(r)
239
+ mu.Lock()
240
+ defer mu.Unlock()
241
+ f, _ := os.OpenFile(ResultPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o644)
242
+ defer f.Close()
243
+ fmt.Fprintf(f, "%s\n", b)
244
+ }
245
+ ````
246
+
247
+ ## 与其他规范的关系
248
+
249
+ | 规范 | 关系 |
250
+ |------|------|
251
+ | `test-writer` Skill | 定义用例 ID(`UT-xx` / `ST-xx`),是 JSONL 中 `id` 字段的来源 |
252
+ | `test-orchestrator` Skill | API 编排测试也可产出同格式 JSONL |
253
+ | `directory-convention.md` | 定义 `logos/resources/verify/` 目录位置 |
254
+ | `logos.config.json` | `verify.result_path` 可覆盖默认路径 |
255
+ | `openlogos verify` 命令 | 读取此格式文件,生成验收报告 |