@sun-asterisk/sungen 3.0.0 → 3.1.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.
Files changed (161) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +24 -0
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts.map +1 -1
  5. package/dist/cli/commands/delivery.js +30 -14
  6. package/dist/cli/commands/delivery.js.map +1 -1
  7. package/dist/cli/commands/eval.d.ts +3 -0
  8. package/dist/cli/commands/eval.d.ts.map +1 -0
  9. package/dist/cli/commands/eval.js +37 -0
  10. package/dist/cli/commands/eval.js.map +1 -0
  11. package/dist/cli/commands/ingest.d.ts +3 -0
  12. package/dist/cli/commands/ingest.d.ts.map +1 -0
  13. package/dist/cli/commands/ingest.js +179 -0
  14. package/dist/cli/commands/ingest.js.map +1 -0
  15. package/dist/cli/index.js +4 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/dashboard/templates/index.html +108 -194
  18. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
  19. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  20. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
  21. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  23. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  24. package/dist/generators/test-generator/code-generator.d.ts +4 -0
  25. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  26. package/dist/generators/test-generator/code-generator.js +31 -2
  27. package/dist/generators/test-generator/code-generator.js.map +1 -1
  28. package/dist/generators/test-generator/patterns/database-patterns.d.ts +5 -0
  29. package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -0
  30. package/dist/generators/test-generator/patterns/database-patterns.js +94 -0
  31. package/dist/generators/test-generator/patterns/database-patterns.js.map +1 -0
  32. package/dist/generators/test-generator/patterns/index.d.ts +1 -0
  33. package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
  34. package/dist/generators/test-generator/patterns/index.js +6 -1
  35. package/dist/generators/test-generator/patterns/index.js.map +1 -1
  36. package/dist/generators/test-generator/template-engine.d.ts +1 -0
  37. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  38. package/dist/generators/test-generator/template-engine.js +1 -1
  39. package/dist/generators/test-generator/template-engine.js.map +1 -1
  40. package/dist/harness/audit.d.ts +16 -0
  41. package/dist/harness/audit.d.ts.map +1 -1
  42. package/dist/harness/audit.js +69 -5
  43. package/dist/harness/audit.js.map +1 -1
  44. package/dist/harness/capability-plan.d.ts +6 -0
  45. package/dist/harness/capability-plan.d.ts.map +1 -1
  46. package/dist/harness/capability-plan.js +14 -1
  47. package/dist/harness/capability-plan.js.map +1 -1
  48. package/dist/harness/catalog/drivers.yaml +1 -1
  49. package/dist/harness/catalog/universal-viewpoints.yaml +1 -1
  50. package/dist/harness/eval/skill-lint.d.ts +16 -0
  51. package/dist/harness/eval/skill-lint.d.ts.map +1 -0
  52. package/dist/harness/eval/skill-lint.js +129 -0
  53. package/dist/harness/eval/skill-lint.js.map +1 -0
  54. package/dist/harness/flow-plan.js +1 -1
  55. package/dist/harness/parse.d.ts +6 -0
  56. package/dist/harness/parse.d.ts.map +1 -1
  57. package/dist/harness/parse.js +18 -3
  58. package/dist/harness/parse.js.map +1 -1
  59. package/dist/harness/quality-gates.d.ts +29 -0
  60. package/dist/harness/quality-gates.d.ts.map +1 -0
  61. package/dist/harness/quality-gates.js +183 -0
  62. package/dist/harness/quality-gates.js.map +1 -0
  63. package/dist/harness/script-check.d.ts.map +1 -1
  64. package/dist/harness/script-check.js +4 -1
  65. package/dist/harness/script-check.js.map +1 -1
  66. package/dist/harness/sensors.d.ts.map +1 -1
  67. package/dist/harness/sensors.js +85 -6
  68. package/dist/harness/sensors.js.map +1 -1
  69. package/dist/harness/spec-coverage.d.ts +37 -0
  70. package/dist/harness/spec-coverage.d.ts.map +1 -0
  71. package/dist/harness/spec-coverage.js +159 -0
  72. package/dist/harness/spec-coverage.js.map +1 -0
  73. package/dist/harness/viewpoint-ledger.d.ts +23 -0
  74. package/dist/harness/viewpoint-ledger.d.ts.map +1 -0
  75. package/dist/harness/viewpoint-ledger.js +118 -0
  76. package/dist/harness/viewpoint-ledger.js.map +1 -0
  77. package/dist/ingest/baseline-audit.d.ts +38 -0
  78. package/dist/ingest/baseline-audit.d.ts.map +1 -0
  79. package/dist/ingest/baseline-audit.js +85 -0
  80. package/dist/ingest/baseline-audit.js.map +1 -0
  81. package/dist/ingest/gsheet-fetch.d.ts +9 -0
  82. package/dist/ingest/gsheet-fetch.d.ts.map +1 -0
  83. package/dist/ingest/gsheet-fetch.js +180 -0
  84. package/dist/ingest/gsheet-fetch.js.map +1 -0
  85. package/dist/ingest/index.d.ts +6 -0
  86. package/dist/ingest/index.d.ts.map +1 -0
  87. package/dist/ingest/index.js +22 -0
  88. package/dist/ingest/index.js.map +1 -0
  89. package/dist/ingest/legacy-parser.d.ts +39 -0
  90. package/dist/ingest/legacy-parser.d.ts.map +1 -0
  91. package/dist/ingest/legacy-parser.js +218 -0
  92. package/dist/ingest/legacy-parser.js.map +1 -0
  93. package/dist/ingest/reconcile.d.ts +30 -0
  94. package/dist/ingest/reconcile.d.ts.map +1 -0
  95. package/dist/ingest/reconcile.js +65 -0
  96. package/dist/ingest/reconcile.js.map +1 -0
  97. package/dist/ingest/to-gherkin.d.ts +33 -0
  98. package/dist/ingest/to-gherkin.d.ts.map +1 -0
  99. package/dist/ingest/to-gherkin.js +93 -0
  100. package/dist/ingest/to-gherkin.js.map +1 -0
  101. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  102. package/dist/orchestrator/ai-rules-updater.js +2 -0
  103. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  104. package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +1 -0
  105. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
  106. package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
  107. package/dist/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
  108. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +25 -1
  109. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
  110. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
  111. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
  112. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
  113. package/dist/orchestrator/templates/specs-db.d.ts +18 -0
  114. package/dist/orchestrator/templates/specs-db.d.ts.map +1 -0
  115. package/dist/orchestrator/templates/specs-db.js +171 -0
  116. package/dist/orchestrator/templates/specs-db.js.map +1 -0
  117. package/dist/orchestrator/templates/specs-db.ts +147 -0
  118. package/docs/orchestration-spec.md +3 -3
  119. package/package.json +4 -4
  120. package/src/cli/commands/audit.ts +19 -0
  121. package/src/cli/commands/delivery.ts +31 -15
  122. package/src/cli/commands/eval.ts +28 -0
  123. package/src/cli/commands/ingest.ts +141 -0
  124. package/src/cli/index.ts +4 -0
  125. package/src/dashboard/templates/index.html +108 -194
  126. package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
  127. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  128. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  129. package/src/generators/test-generator/code-generator.ts +29 -2
  130. package/src/generators/test-generator/patterns/database-patterns.ts +95 -0
  131. package/src/generators/test-generator/patterns/index.ts +3 -0
  132. package/src/generators/test-generator/template-engine.ts +2 -2
  133. package/src/harness/audit.ts +82 -5
  134. package/src/harness/capability-plan.ts +12 -1
  135. package/src/harness/catalog/drivers.yaml +1 -1
  136. package/src/harness/catalog/universal-viewpoints.yaml +1 -1
  137. package/src/harness/eval/skill-lint.ts +87 -0
  138. package/src/harness/flow-plan.ts +1 -1
  139. package/src/harness/parse.ts +19 -3
  140. package/src/harness/quality-gates.ts +152 -0
  141. package/src/harness/script-check.ts +4 -1
  142. package/src/harness/sensors.ts +84 -7
  143. package/src/harness/spec-coverage.ts +139 -0
  144. package/src/harness/viewpoint-ledger.ts +80 -0
  145. package/src/ingest/baseline-audit.ts +100 -0
  146. package/src/ingest/gsheet-fetch.ts +152 -0
  147. package/src/ingest/index.ts +5 -0
  148. package/src/ingest/legacy-parser.ts +184 -0
  149. package/src/ingest/reconcile.ts +80 -0
  150. package/src/ingest/to-gherkin.ts +108 -0
  151. package/src/orchestrator/ai-rules-updater.ts +2 -0
  152. package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +1 -0
  153. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
  154. package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
  155. package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
  156. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +25 -1
  157. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
  158. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
  159. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
  160. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
  161. package/src/orchestrator/templates/specs-db.ts +147 -0
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.db = void 0;
37
+ /* eslint-disable */
38
+ /**
39
+ * Sungen Data Driver — runtime DB-verification helper (auto-generated into specs/db.ts).
40
+ *
41
+ * Read-only by construction: it only ever issues a single parameterized SELECT, and
42
+ * table/column identifiers are validated against a strict allowlist pattern (identifiers
43
+ * can't be bound as parameters). Secrets come from .env.qa / process.env, never inline.
44
+ *
45
+ * Engines: PostgreSQL (`pg`) and SQLite (`better-sqlite3`), lazy-loaded on first use.
46
+ * Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
47
+ *
48
+ * DO NOT EDIT — regenerated by `sungen generate`.
49
+ */
50
+ const test_1 = require("@playwright/test");
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
54
+ const ident = (s) => {
55
+ if (!IDENT.test(s))
56
+ throw new Error(`Unsafe identifier: ${JSON.stringify(s)} (allowed: [A-Za-z_][A-Za-z0-9_]*)`);
57
+ return s;
58
+ };
59
+ function loadEnvQa() {
60
+ for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
61
+ const p = path.join(process.cwd(), name);
62
+ if (!name.endsWith('.') && fs.existsSync(p)) {
63
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
64
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
65
+ if (m && process.env[m[1]] === undefined) {
66
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
67
+ }
68
+ }
69
+ }
70
+ }
71
+ }
72
+ function loadConfig() {
73
+ loadEnvQa();
74
+ const file = [
75
+ path.join(process.cwd(), 'datasources.yaml'),
76
+ path.join(process.cwd(), 'qa', 'datasources.yaml'),
77
+ ].find((f) => fs.existsSync(f));
78
+ if (!file)
79
+ throw new Error('Data Driver: no datasources.yaml found (project root or qa/).');
80
+ let raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
81
+ const { parse } = require('yaml');
82
+ const doc = parse(raw) || {};
83
+ if (!doc.datasources || typeof doc.datasources !== 'object') {
84
+ throw new Error('Data Driver: datasources.yaml must define a top-level `datasources:` map.');
85
+ }
86
+ return doc.datasources;
87
+ }
88
+ class DataSource {
89
+ constructor() {
90
+ this.configs = null;
91
+ this.engines = new Map();
92
+ }
93
+ cfg(name) {
94
+ if (!this.configs)
95
+ this.configs = loadConfig();
96
+ const key = name || Object.keys(this.configs)[0];
97
+ const conf = this.configs[key];
98
+ if (!conf)
99
+ throw new Error(`Data Driver: datasource "${key}" not found in datasources.yaml`);
100
+ return { key, conf };
101
+ }
102
+ async engine(name) {
103
+ const { key, conf } = this.cfg(name);
104
+ if (this.engines.has(key))
105
+ return { engine: this.engines.get(key), conf };
106
+ if (!conf.url)
107
+ throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
108
+ let engine;
109
+ if (conf.engine === 'postgres') {
110
+ const { Pool } = require('pg');
111
+ const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
112
+ engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
113
+ }
114
+ else if (conf.engine === 'sqlite') {
115
+ const Database = require('better-sqlite3');
116
+ const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
117
+ engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
118
+ }
119
+ else {
120
+ throw new Error(`Data Driver: engine "${conf.engine}" not supported yet (postgres | sqlite).`);
121
+ }
122
+ this.engines.set(key, engine);
123
+ return { engine, conf };
124
+ }
125
+ build(table, filter) {
126
+ const cols = Object.keys(filter);
127
+ const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
128
+ const sql = `SELECT * FROM ${ident(table)}${where ? ' WHERE ' + where : ''} LIMIT 50`;
129
+ return { sql, params: cols.map((c) => filter[c]) };
130
+ }
131
+ /** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
132
+ async assertRow(table, filter, expected, datasource) {
133
+ const { engine, conf } = await this.engine(datasource);
134
+ const { sql, params } = this.build(table, filter);
135
+ const rows = await engine.query(this.sqlFor(conf, sql), params);
136
+ (0, test_1.expect)(rows.length, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBeGreaterThanOrEqual(1);
137
+ if (expected) {
138
+ const row = rows[0];
139
+ for (const [col, val] of Object.entries(expected)) {
140
+ ident(col);
141
+ (0, test_1.expect)(String(row[col]), `Column "${col}" of "${table}" where ${desc(filter)}`).toBe(String(val));
142
+ }
143
+ }
144
+ }
145
+ /** No row matching `filter` may exist. */
146
+ async assertNoRow(table, filter, datasource) {
147
+ const { engine, conf } = await this.engine(datasource);
148
+ const { sql, params } = this.build(table, filter);
149
+ const rows = await engine.query(this.sqlFor(conf, sql), params);
150
+ (0, test_1.expect)(rows.length, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBe(0);
151
+ }
152
+ /** Exactly `count` rows must match `filter`. */
153
+ async assertCount(table, filter, count, datasource) {
154
+ const { engine, conf } = await this.engine(datasource);
155
+ const cols = Object.keys(filter);
156
+ const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
157
+ const sql = `SELECT count(*) AS n FROM ${ident(table)}${where ? ' WHERE ' + where : ''}`;
158
+ const rows = await engine.query(this.sqlFor(conf, sql), cols.map((c) => filter[c]));
159
+ const n = Number(rows[0]?.n ?? rows[0]?.['count(*)'] ?? 0);
160
+ (0, test_1.expect)(n, `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`).toBe(Number(count));
161
+ }
162
+ /** Rewrites $1/$2 placeholders to `?` for SQLite. */
163
+ sqlFor(conf, sql) {
164
+ return conf.engine === 'sqlite' ? sql.replace(/\$\d+/g, '?') : sql;
165
+ }
166
+ }
167
+ function desc(filter) {
168
+ return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
169
+ }
170
+ exports.db = new DataSource();
171
+ //# sourceMappingURL=specs-db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-db.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Sungen Data Driver — runtime DB-verification helper (auto-generated into specs/db.ts).
4
+ *
5
+ * Read-only by construction: it only ever issues a single parameterized SELECT, and
6
+ * table/column identifiers are validated against a strict allowlist pattern (identifiers
7
+ * can't be bound as parameters). Secrets come from .env.qa / process.env, never inline.
8
+ *
9
+ * Engines: PostgreSQL (`pg`) and SQLite (`better-sqlite3`), lazy-loaded on first use.
10
+ * Config: a `datasources.yaml` at the project root (or qa/), with ${VAR} resolved from env.
11
+ *
12
+ * DO NOT EDIT — regenerated by `sungen generate`.
13
+ */
14
+ import { expect } from '@playwright/test';
15
+ import * as fs from 'fs';
16
+ import * as path from 'path';
17
+
18
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
19
+ const ident = (s: string): string => {
20
+ if (!IDENT.test(s)) throw new Error(`Unsafe identifier: ${JSON.stringify(s)} (allowed: [A-Za-z_][A-Za-z0-9_]*)`);
21
+ return s;
22
+ };
23
+
24
+ interface DataSourceConfig {
25
+ engine: 'postgres' | 'mysql' | 'sqlite';
26
+ url: string;
27
+ readonly?: boolean;
28
+ statement_timeout_ms?: number;
29
+ max_rows?: number;
30
+ }
31
+
32
+ function loadEnvQa(): void {
33
+ for (const name of ['.env.qa', `.env.qa.${process.env.SUNGEN_ENV || ''}`]) {
34
+ const p = path.join(process.cwd(), name);
35
+ if (!name.endsWith('.') && fs.existsSync(p)) {
36
+ for (const line of fs.readFileSync(p, 'utf8').split('\n')) {
37
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
38
+ if (m && process.env[m[1]] === undefined) {
39
+ process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+
46
+ function loadConfig(): Record<string, DataSourceConfig> {
47
+ loadEnvQa();
48
+ const file = [
49
+ path.join(process.cwd(), 'datasources.yaml'),
50
+ path.join(process.cwd(), 'qa', 'datasources.yaml'),
51
+ ].find((f) => fs.existsSync(f));
52
+ if (!file) throw new Error('Data Driver: no datasources.yaml found (project root or qa/).');
53
+ let raw = fs.readFileSync(file, 'utf8').replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_, k) => process.env[k] ?? '');
54
+ const { parse } = require('yaml');
55
+ const doc = parse(raw) || {};
56
+ if (!doc.datasources || typeof doc.datasources !== 'object') {
57
+ throw new Error('Data Driver: datasources.yaml must define a top-level `datasources:` map.');
58
+ }
59
+ return doc.datasources;
60
+ }
61
+
62
+ type Engine = { query(sql: string, params: any[]): Promise<any[]>; };
63
+
64
+ class DataSource {
65
+ private configs: Record<string, DataSourceConfig> | null = null;
66
+ private engines = new Map<string, Engine>();
67
+
68
+ private cfg(name?: string): { key: string; conf: DataSourceConfig } {
69
+ if (!this.configs) this.configs = loadConfig();
70
+ const key = name || Object.keys(this.configs)[0];
71
+ const conf = this.configs[key];
72
+ if (!conf) throw new Error(`Data Driver: datasource "${key}" not found in datasources.yaml`);
73
+ return { key, conf };
74
+ }
75
+
76
+ private async engine(name?: string): Promise<{ engine: Engine; conf: DataSourceConfig }> {
77
+ const { key, conf } = this.cfg(name);
78
+ if (this.engines.has(key)) return { engine: this.engines.get(key)!, conf };
79
+ if (!conf.url) throw new Error(`Data Driver: datasource "${key}" has no url (set it in .env.qa).`);
80
+ let engine: Engine;
81
+ if (conf.engine === 'postgres') {
82
+ const { Pool } = require('pg');
83
+ const pool = new Pool({ connectionString: conf.url, max: 2, statement_timeout: conf.statement_timeout_ms ?? 4000 });
84
+ engine = { query: async (sql, params) => (await pool.query(sql, params)).rows };
85
+ } else if (conf.engine === 'sqlite') {
86
+ const Database = require('better-sqlite3');
87
+ const db = new Database(conf.url.replace(/^sqlite:/, ''), { readonly: conf.readonly !== false });
88
+ engine = { query: async (sql, params) => db.prepare(sql).all(...params) };
89
+ } else {
90
+ throw new Error(`Data Driver: engine "${conf.engine}" not supported yet (postgres | sqlite).`);
91
+ }
92
+ this.engines.set(key, engine);
93
+ return { engine, conf };
94
+ }
95
+
96
+ private build(table: string, filter: Record<string, any>): { sql: string; params: any[] } {
97
+ const cols = Object.keys(filter);
98
+ const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
99
+ const sql = `SELECT * FROM ${ident(table)}${where ? ' WHERE ' + where : ''} LIMIT 50`;
100
+ return { sql, params: cols.map((c) => filter[c]) };
101
+ }
102
+
103
+ /** A row matching `filter` must exist; if `expected` given, assert those columns on the first match. */
104
+ async assertRow(table: string, filter: Record<string, any>, expected?: Record<string, any>, datasource?: string): Promise<void> {
105
+ const { engine, conf } = await this.engine(datasource);
106
+ const { sql, params } = this.build(table, filter);
107
+ const rows = await engine.query(this.sqlFor(conf, sql), params);
108
+ expect(rows.length, `Expected a row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBeGreaterThanOrEqual(1);
109
+ if (expected) {
110
+ const row = rows[0];
111
+ for (const [col, val] of Object.entries(expected)) {
112
+ ident(col);
113
+ expect(String(row[col]), `Column "${col}" of "${table}" where ${desc(filter)}`).toBe(String(val));
114
+ }
115
+ }
116
+ }
117
+
118
+ /** No row matching `filter` may exist. */
119
+ async assertNoRow(table: string, filter: Record<string, any>, datasource?: string): Promise<void> {
120
+ const { engine, conf } = await this.engine(datasource);
121
+ const { sql, params } = this.build(table, filter);
122
+ const rows = await engine.query(this.sqlFor(conf, sql), params);
123
+ expect(rows.length, `Expected NO row in "${table}" where ${desc(filter)} — found ${rows.length}`).toBe(0);
124
+ }
125
+
126
+ /** Exactly `count` rows must match `filter`. */
127
+ async assertCount(table: string, filter: Record<string, any>, count: number, datasource?: string): Promise<void> {
128
+ const { engine, conf } = await this.engine(datasource);
129
+ const cols = Object.keys(filter);
130
+ const where = cols.map((c, i) => `${ident(c)} = $${i + 1}`).join(' AND ');
131
+ const sql = `SELECT count(*) AS n FROM ${ident(table)}${where ? ' WHERE ' + where : ''}`;
132
+ const rows = await engine.query(this.sqlFor(conf, sql), cols.map((c) => filter[c]));
133
+ const n = Number(rows[0]?.n ?? rows[0]?.['count(*)'] ?? 0);
134
+ expect(n, `Expected ${count} row(s) in "${table}"${cols.length ? ' where ' + desc(filter) : ''} — found ${n}`).toBe(Number(count));
135
+ }
136
+
137
+ /** Rewrites $1/$2 placeholders to `?` for SQLite. */
138
+ private sqlFor(conf: DataSourceConfig, sql: string): string {
139
+ return conf.engine === 'sqlite' ? sql.replace(/\$\d+/g, '?') : sql;
140
+ }
141
+ }
142
+
143
+ function desc(filter: Record<string, any>): string {
144
+ return Object.entries(filter).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
145
+ }
146
+
147
+ export const db = new DataSource();
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Spec triển khai cho lần refactor lớn: chuyển từ **"Context Engine + Skill" (workflow tuyến tính)** sang **Orchestration + Harness Engine**.
4
4
  > Mục tiêu chính: **nâng chất lượng testcase** (đo & gate thay vì nhờ AI). Bất biến: `generate`, auto-fix selector, `delivery`, reports.
5
- > Tài liệu nền: `reports/sungen_direction_solution.md`, `reports/sungen_refactor_spec.md`, `reports/sungen_home_gherkin_viewpoint_coverage_review.md`.
5
+ > Tài liệu nền: `docs/spec/sungen_direction_solution.md`, `docs/spec/sungen_refactor_spec.md`.
6
6
 
7
7
  ---
8
8
 
@@ -153,7 +153,7 @@ Hết ngân sách repair (N vòng) mà vẫn FAIL:
153
153
  └─► KHÔNG lặp vô hạn. Ghi gap vào audit-report + đưa QA checkpoint (human-in-the-loop).
154
154
  ```
155
155
 
156
- **Quy tắc "khám phá thêm khi nông":** nếu assertion-depth sensor báo một viewpoint critical chỉ assert "see page/section" (vụ home: Cart/Detail/Filter), Orchestrator phải nhận diện đây là **cross-screen** → khuyến nghị chuyển sang **flow** (`add-flow`) và/hoặc cần **năng lực DSL capture biến** (xem `reports/sungen_refactor_spec.md` §5.4) — thay vì giả vờ pass bằng assertion nông.
156
+ **Quy tắc "khám phá thêm khi nông":** nếu assertion-depth sensor báo một viewpoint critical chỉ assert "see page/section" (vụ home: Cart/Detail/Filter), Orchestrator phải nhận diện đây là **cross-screen** → khuyến nghị chuyển sang **flow** (`add-flow`) và/hoặc cần **năng lực DSL capture biến** (xem `docs/spec/sungen_refactor_spec.md` §5.4) — thay vì giả vờ pass bằng assertion nông.
157
157
 
158
158
  ---
159
159
 
@@ -180,7 +180,7 @@ Bốn sensor + một gate. Input: `.feature` (qua GherkinParser) + `test-viewpoi
180
180
  - Orchestrator đưa phản hồi này lại bước sinh. **Ngân sách N vòng** (mặc định 2–3, như vòng auto-fix selector). Hết ngân sách → báo gap, không lặp vô hạn.
181
181
 
182
182
  ### 5.4 QA Checkpoint
183
- - QA accept/reject/edit/add viewpoint. Mọi quyết định ghi `feedback.jsonl` (xem `reports/sungen_refactor_spec.md` §8).
183
+ - QA accept/reject/edit/add viewpoint. Mọi quyết định ghi `feedback.jsonl` (xem `docs/spec/sungen_refactor_spec.md` §8).
184
184
 
185
185
  ---
186
186
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -12,8 +12,8 @@
12
12
  "copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
13
13
  "build:dashboard": "cd dashboard && npm install --silent && npm run build && cd .. && cp dashboard/dist/index.html src/dashboard/templates/index.html",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts",
16
- "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts",
16
+ "test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
17
17
  "prepublishOnly": "npm run build:dashboard && npm run build"
18
18
  },
19
19
  "keywords": [
@@ -82,7 +82,7 @@
82
82
  "dist",
83
83
  "bin",
84
84
  "src",
85
- "docs",
85
+ "docs/orchestration-spec.md",
86
86
  "README.md",
87
87
  "LICENSE"
88
88
  ]
@@ -58,6 +58,25 @@ function render(r: AuditReport): void {
58
58
  L(` ⑥ Traceability — ${(r.trace.mappedRatio * 100).toFixed(0)}% scenarios linked to viewpoint-overview`);
59
59
  L(` ${r.trace.note}`);
60
60
  L('');
61
+ if (r.spec.hasSpec && (r.spec.frTotal > 0 || r.spec.triggerGaps.length > 0 || r.spec.verdict !== 'pass')) {
62
+ L(` ⑦ Spec coverage — FR ${r.spec.frCovered}/${r.spec.frTotal} covered [${r.spec.verdict.toUpperCase()}]`);
63
+ for (const g of r.spec.triggerGaps) L(` ✗ TRIGGER-UNCOVERED: "${g.constraint}"${g.code ? ` (${g.code})` : ''} mandated on [${g.required.join(', ')}], only tested on [${g.found.join(', ') || 'none'}] → missing ${g.missing.join(', ')}`);
64
+ for (const u of r.spec.uncoveredMust.slice(0, 6)) L(` ✗ SPEC-UNCOVERED: ${u.id} (MUST) — "${u.text}"`);
65
+ if (!r.spec.triggerGaps.length && !r.spec.uncoveredMust.length) L(' ✓ every MUST FR + per-constraint trigger covered');
66
+ L('');
67
+ }
68
+ if (r.ledger.hasViewpoint && r.ledger.total > 0) {
69
+ L(` ⑧ Viewpoint atomic coverage — ${r.ledger.covered}/${r.ledger.total} items (${(r.ledger.ratio * 100).toFixed(0)}%)`);
70
+ for (const m of r.ledger.missing.slice(0, 8)) L(` ○ missing: ${m.id ? m.id + ' — ' : ''}${m.text.slice(0, 70)}`);
71
+ if (r.ledger.missing.length > 8) L(` … +${r.ledger.missing.length - 8} more`);
72
+ L('');
73
+ }
74
+ if (r.calibration) {
75
+ const ax = Object.entries(r.calibration.axes).map(([k, v]) => `${k}=${(v * 100).toFixed(0)}%`).join(' · ');
76
+ L(` ⑨ Calibration — ${ax}`);
77
+ L(` weakest: ${r.calibration.weakest.axis} ${(r.calibration.weakest.value * 100).toFixed(0)}%${r.calibration.inflated ? ' ⚠ SCORE-INFLATED-BY-BREADTH' : ''}`);
78
+ L('');
79
+ }
61
80
  L(' ── Findings (Repair targets) ──');
62
81
  if (r.findings.length === 0) L(' ✓ none — output passes the harness');
63
82
  for (const f of r.findings) L(` • ${f}`);
@@ -20,7 +20,7 @@ import {
20
20
  renderCsv,
21
21
  writeCsv,
22
22
  } from '../../exporters/csv-exporter';
23
- import { renderXlsx, renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
23
+ import { renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
24
24
  import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow } from '../../exporters/types';
25
25
 
26
26
  const COLOR = {
@@ -421,7 +421,14 @@ async function exportTarget(
421
421
  const tempSummary = buildSummary(label, rows, '');
422
422
  const csv = renderCsv(tempSummary, rows, specLink);
423
423
  const csvPath = writeCsv(cwd, target.featureBaseName, csv);
424
- const wb = renderXlsx(tempSummary, rows, specLink);
424
+ // XLSX: two sheets — "Auto" (automatable: Auto + Not compiled) and "Manual" (@manual) —
425
+ // so QA manages the automated vs manual test-case sets separately. (CSV keeps every row.)
426
+ const autoRows = rows.filter((r) => r.testcaseType !== 'Manual');
427
+ const manualRows = rows.filter((r) => r.testcaseType === 'Manual');
428
+ const wb = renderXlsxMultiSheet([
429
+ { sheetName: 'Auto', summary: buildSummary(label, autoRows, ''), rows: autoRows, specLink },
430
+ { sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
431
+ ]);
425
432
  await writeXlsx(cwd, target.featureBaseName, wb);
426
433
  return buildSummary(label, rows, path.relative(cwd, csvPath));
427
434
  }
@@ -429,7 +436,12 @@ async function exportTarget(
429
436
  const variants = discoverLocaleVariants(cwd, target);
430
437
  let primarySummary: ScreenSummary | null = null;
431
438
  let primaryCsvPath = '';
432
- const sheets: { sheetName: string; summary: ScreenSummary; rows: TestCaseRow[]; specLink: string }[] = [];
439
+ // XLSX is split by automation type: an "Auto" sheet (automatable TCs, results differ per
440
+ // locale) and a single shared "Manual" sheet (@manual TCs don't execute and are the same
441
+ // across locales). With multiple locales, Auto sheets are prefixed by locale code.
442
+ const autoSheets: { sheetName: string; summary: ScreenSummary; rows: TestCaseRow[]; specLink: string }[] = [];
443
+ let manualRows: TestCaseRow[] = [];
444
+ const multiLocale = variants.length > 1;
433
445
 
434
446
  for (const variant of variants) {
435
447
  // For the base variant the overlay merge is skipped (`locale: null`);
@@ -446,33 +458,37 @@ async function exportTarget(
446
458
  env,
447
459
  selectorKeyMap,
448
460
  });
449
- const variantSummary = buildSummary(label, variantRows, '');
450
461
 
451
- // CSV: always one file per locale (CSV has no sheet concept).
462
+ // CSV: always one file per locale, every row (CSV has no sheet concept).
452
463
  const csvLocale = variant.locale || null; // '' or 'en' → '' / 'en'
453
- const csv = renderCsv(variantSummary, variantRows, specLink);
464
+ const csv = renderCsv(buildSummary(label, variantRows, ''), variantRows, specLink);
454
465
  const csvPath = writeCsv(cwd, target.featureBaseName, csv, csvLocale);
455
466
 
456
- sheets.push({
457
- sheetName: `${target.featureBaseName}-${variant.displayCode}`,
458
- summary: variantSummary,
459
- rows: variantRows,
467
+ const autoRows = variantRows.filter((r) => r.testcaseType !== 'Manual');
468
+ autoSheets.push({
469
+ sheetName: multiLocale ? `${variant.displayCode} Auto` : 'Auto',
470
+ summary: buildSummary(label, autoRows, ''),
471
+ rows: autoRows,
460
472
  specLink,
461
473
  });
462
474
 
463
- // Use the base variant's summary as the "primary" return value so the
464
- // top-level reporter rolls up base-locale numbers.
475
+ // Use the base variant for the shared Manual sheet + the rolled-up "primary" summary.
465
476
  if (variant.locale === '') {
477
+ manualRows = variantRows.filter((r) => r.testcaseType === 'Manual');
466
478
  primarySummary = buildSummary(label, variantRows, path.relative(cwd, csvPath));
467
479
  primaryCsvPath = csvPath;
468
480
  }
469
481
  }
470
482
 
471
- // XLSX: single-sheet when only base, multi-sheet when 2+ locales found.
472
- const wb = sheets.length >= 2 ? renderXlsxMultiSheet(sheets) : renderXlsx(sheets[0].summary, sheets[0].rows, specLink);
483
+ // All Auto sheets, then one "Manual" sheet always present for a predictable structure.
484
+ const sheets = [
485
+ ...autoSheets,
486
+ { sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
487
+ ];
488
+ const wb = renderXlsxMultiSheet(sheets);
473
489
  await writeXlsx(cwd, target.featureBaseName, wb);
474
490
 
475
- return primarySummary ?? buildSummary(label, sheets[0].rows, primaryCsvPath);
491
+ return primarySummary ?? buildSummary(label, (autoSheets[0]?.rows ?? []).concat(manualRows), primaryCsvPath);
476
492
  } catch (err) {
477
493
  console.error(`${COLOR.red}Error exporting ${label}:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
478
494
  return null;
@@ -0,0 +1,28 @@
1
+ import { Command } from 'commander';
2
+ import { lintSkills, defaultSkillDir } from '../../harness/eval/skill-lint';
3
+
4
+ export function registerEvalCommand(program: Command): void {
5
+ program
6
+ .command('eval')
7
+ .description('Eval harness: quality checks on Sungen\'s own skills/instructions (dev/CI)')
8
+ .option('--skills', 'Static skill-lint: frontmatter, line budget, claude↔github sync, registration')
9
+ .option('--dir <path>', 'Templates dir to lint (default: bundled ai-instructions)')
10
+ .option('--json', 'Output the raw findings JSON')
11
+ .action((options) => {
12
+ try {
13
+ if (!options.skills) throw new Error('Provide --skills (the only eval mode today)');
14
+ const dir = options.dir || defaultSkillDir();
15
+ const r = lintSkills(dir);
16
+ if (options.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.errors > 0 ? 2 : 0); }
17
+ console.log('');
18
+ console.log(`━━━ Skill-lint: ${r.checked} skill template(s) ━━━`);
19
+ if (!r.findings.length) console.log(' ✓ all skills pass (frontmatter · line-budget · variant-sync · registration)');
20
+ for (const f of r.findings) console.log(` ${f.level === 'error' ? '✗' : '⚠'} [${f.rule}] ${f.file} — ${f.detail}`);
21
+ console.log('');
22
+ process.exit(r.errors > 0 ? 2 : 0);
23
+ } catch (error) {
24
+ console.error('Error:', error instanceof Error ? error.message : error);
25
+ process.exit(1);
26
+ }
27
+ });
28
+ }