@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.
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +24 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +30 -14
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/eval.d.ts +3 -0
- package/dist/cli/commands/eval.d.ts.map +1 -0
- package/dist/cli/commands/eval.js +37 -0
- package/dist/cli/commands/eval.js.map +1 -0
- package/dist/cli/commands/ingest.d.ts +3 -0
- package/dist/cli/commands/ingest.d.ts.map +1 -0
- package/dist/cli/commands/ingest.js +179 -0
- package/dist/cli/commands/ingest.js.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/templates/index.html +108 -194
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/code-generator.d.ts +4 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +31 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +5 -0
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/database-patterns.js +94 -0
- package/dist/generators/test-generator/patterns/database-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +6 -1
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/harness/audit.d.ts +16 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +69 -5
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +6 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +14 -1
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +1 -1
- package/dist/harness/eval/skill-lint.d.ts +16 -0
- package/dist/harness/eval/skill-lint.d.ts.map +1 -0
- package/dist/harness/eval/skill-lint.js +129 -0
- package/dist/harness/eval/skill-lint.js.map +1 -0
- package/dist/harness/flow-plan.js +1 -1
- package/dist/harness/parse.d.ts +6 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +18 -3
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts +29 -0
- package/dist/harness/quality-gates.d.ts.map +1 -0
- package/dist/harness/quality-gates.js +183 -0
- package/dist/harness/quality-gates.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +4 -1
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +85 -6
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +37 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -0
- package/dist/harness/spec-coverage.js +159 -0
- package/dist/harness/spec-coverage.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts +23 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -0
- package/dist/harness/viewpoint-ledger.js +118 -0
- package/dist/harness/viewpoint-ledger.js.map +1 -0
- package/dist/ingest/baseline-audit.d.ts +38 -0
- package/dist/ingest/baseline-audit.d.ts.map +1 -0
- package/dist/ingest/baseline-audit.js +85 -0
- package/dist/ingest/baseline-audit.js.map +1 -0
- package/dist/ingest/gsheet-fetch.d.ts +9 -0
- package/dist/ingest/gsheet-fetch.d.ts.map +1 -0
- package/dist/ingest/gsheet-fetch.js +180 -0
- package/dist/ingest/gsheet-fetch.js.map +1 -0
- package/dist/ingest/index.d.ts +6 -0
- package/dist/ingest/index.d.ts.map +1 -0
- package/dist/ingest/index.js +22 -0
- package/dist/ingest/index.js.map +1 -0
- package/dist/ingest/legacy-parser.d.ts +39 -0
- package/dist/ingest/legacy-parser.d.ts.map +1 -0
- package/dist/ingest/legacy-parser.js +218 -0
- package/dist/ingest/legacy-parser.js.map +1 -0
- package/dist/ingest/reconcile.d.ts +30 -0
- package/dist/ingest/reconcile.d.ts.map +1 -0
- package/dist/ingest/reconcile.js +65 -0
- package/dist/ingest/reconcile.js.map +1 -0
- package/dist/ingest/to-gherkin.d.ts +33 -0
- package/dist/ingest/to-gherkin.d.ts.map +1 -0
- package/dist/ingest/to-gherkin.js +93 -0
- package/dist/ingest/to-gherkin.js.map +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +2 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +25 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
- package/dist/orchestrator/templates/specs-db.d.ts +18 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-db.js +171 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -0
- package/dist/orchestrator/templates/specs-db.ts +147 -0
- package/docs/orchestration-spec.md +3 -3
- package/package.json +4 -4
- package/src/cli/commands/audit.ts +19 -0
- package/src/cli/commands/delivery.ts +31 -15
- package/src/cli/commands/eval.ts +28 -0
- package/src/cli/commands/ingest.ts +141 -0
- package/src/cli/index.ts +4 -0
- package/src/dashboard/templates/index.html +108 -194
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/code-generator.ts +29 -2
- package/src/generators/test-generator/patterns/database-patterns.ts +95 -0
- package/src/generators/test-generator/patterns/index.ts +3 -0
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/harness/audit.ts +82 -5
- package/src/harness/capability-plan.ts +12 -1
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/catalog/universal-viewpoints.yaml +1 -1
- package/src/harness/eval/skill-lint.ts +87 -0
- package/src/harness/flow-plan.ts +1 -1
- package/src/harness/parse.ts +19 -3
- package/src/harness/quality-gates.ts +152 -0
- package/src/harness/script-check.ts +4 -1
- package/src/harness/sensors.ts +84 -7
- package/src/harness/spec-coverage.ts +139 -0
- package/src/harness/viewpoint-ledger.ts +80 -0
- package/src/ingest/baseline-audit.ts +100 -0
- package/src/ingest/gsheet-fetch.ts +152 -0
- package/src/ingest/index.ts +5 -0
- package/src/ingest/legacy-parser.ts +184 -0
- package/src/ingest/reconcile.ts +80 -0
- package/src/ingest/to-gherkin.ts +108 -0
- package/src/orchestrator/ai-rules-updater.ts +2 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +25 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
- 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: `
|
|
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 `
|
|
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 `
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
464
|
+
const csv = renderCsv(buildSummary(label, variantRows, ''), variantRows, specLink);
|
|
454
465
|
const csvPath = writeCsv(cwd, target.featureBaseName, csv, csvLocale);
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
|
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
|
-
//
|
|
472
|
-
const
|
|
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,
|
|
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
|
+
}
|