@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
|
@@ -63,7 +63,7 @@ export interface TestGeneratorAdapter {
|
|
|
63
63
|
// Template rendering methods
|
|
64
64
|
renderTestFile(data: TestFileData): string;
|
|
65
65
|
renderScenario(data: ScenarioData): string;
|
|
66
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string;
|
|
66
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string;
|
|
67
67
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
68
68
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
69
69
|
renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
|
|
|
26
26
|
return this.templateEngine.renderScenario(data);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
|
|
29
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -3,6 +3,9 @@ import { type Page, type BrowserContext } from '@playwright/test';
|
|
|
3
3
|
{{#if runtimeData}}
|
|
4
4
|
import { TestDataLoader } from '{{basePath}}/test-data';
|
|
5
5
|
{{/if}}
|
|
6
|
+
{{#if needsDb}}
|
|
7
|
+
import { db } from '{{basePath}}/db';
|
|
8
|
+
{{/if}}
|
|
6
9
|
|
|
7
10
|
// This file is auto-generated from Gherkin feature files
|
|
8
11
|
// DO NOT EDIT MANUALLY - changes will be overwritten
|
|
@@ -4,6 +4,7 @@ import { ParsedFeature, ParsedScenario, ParsedStep } from '../gherkin-parser';
|
|
|
4
4
|
import { StepMapper } from './step-mapper';
|
|
5
5
|
import { TestGeneratorAdapter, adapterRegistry } from './adapters';
|
|
6
6
|
import { transformToRuntimeData } from './utils/runtime-data-transformer';
|
|
7
|
+
import { isDbStep } from './patterns/database-patterns';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Filter base scenario steps for @extend: only keep Given→When steps.
|
|
@@ -236,7 +237,11 @@ export class CodeGenerator {
|
|
|
236
237
|
const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
|
|
237
238
|
const needsCleanupImport = !isParallelFeature && hasCleanupTags;
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
// Data Driver: if any step verifies DB state, import the `db` helper + emit specs/db.ts
|
|
241
|
+
const needsDb = this.featureUsesDb(feature);
|
|
242
|
+
if (needsDb) this.ensureDbFile(outputDir);
|
|
243
|
+
|
|
244
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb });
|
|
240
245
|
|
|
241
246
|
// Generate test code (async now to support AI mapping)
|
|
242
247
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -292,8 +297,30 @@ export class CodeGenerator {
|
|
|
292
297
|
/**
|
|
293
298
|
* Ensure specs/base.ts exists in the output directory
|
|
294
299
|
*/
|
|
300
|
+
/** True when any step (background or scenario) in the feature is a DB-verification step. */
|
|
301
|
+
private featureUsesDb(feature: ParsedFeature): boolean {
|
|
302
|
+
const steps: ParsedStep[] = [];
|
|
303
|
+
if (feature.background?.steps) steps.push(...feature.background.steps);
|
|
304
|
+
for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
|
|
305
|
+
return steps.some((s) => s && typeof s.text === 'string' && isDbStep(s.text));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Copy the Data Driver runtime helper into specs/db.ts (idempotent). */
|
|
309
|
+
ensureDbFile(outputDir: string): void {
|
|
310
|
+
const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
|
|
311
|
+
const dbPath = path.join(outputDir, 'db.ts');
|
|
312
|
+
if (!fs.existsSync(dbPath)) {
|
|
313
|
+
const templatePath = path.join(templatesRoot, 'specs-db.ts');
|
|
314
|
+
if (fs.existsSync(templatePath)) {
|
|
315
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
316
|
+
fs.copyFileSync(templatePath, dbPath);
|
|
317
|
+
console.log('✓ Created: specs/db.ts');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
295
322
|
ensureBaseFile(outputDir: string): void {
|
|
296
|
-
const templatesRoot = path.join(__dirname, '..', '..', '
|
|
323
|
+
const templatesRoot = path.join(__dirname, '..', '..', 'orchestrator', 'templates');
|
|
297
324
|
|
|
298
325
|
const basePath = path.join(outputDir, 'base.ts');
|
|
299
326
|
if (!fs.existsSync(basePath)) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ParsedStep } from '../../gherkin-parser';
|
|
2
|
+
import { StepPattern, PatternContext } from './types';
|
|
3
|
+
import { MappedStep } from '../step-mapper';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database verification patterns (Data Driver v1) — declarative, no-SQL DB assertions
|
|
7
|
+
* that compile to calls on the runtime `db` helper (specs/db.ts). Read-only.
|
|
8
|
+
*
|
|
9
|
+
* User see [users] row where [email] is {{reg_email}}
|
|
10
|
+
* User see [users] row where [email] is {{reg_email}} has [status] = "active"
|
|
11
|
+
* User see [users] no row where [email] is {{dup_email}}
|
|
12
|
+
* User see [orders] where [buyer] is {{buyer}} count is {{expected_count}}
|
|
13
|
+
*
|
|
14
|
+
* Identifiers ([table]/[column]) are validated by the helper; values bind as parameters.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const TABLE = String.raw`\[([A-Za-z_][A-Za-z0-9_]*)\]`;
|
|
18
|
+
const VALUE = String.raw`\{\{[^}]+\}\}|"[^"]*"|'[^']*'|-?\d+(?:\.\d+)?`;
|
|
19
|
+
|
|
20
|
+
const reRow = new RegExp(`see\\s+${TABLE}\\s+row\\s+where\\b`, 'i');
|
|
21
|
+
const reNoRow = new RegExp(`see\\s+${TABLE}\\s+no\\s+row\\s+where\\b`, 'i');
|
|
22
|
+
const reCount = new RegExp(`see\\s+${TABLE}.*\\bcount\\s+is\\b`, 'i');
|
|
23
|
+
|
|
24
|
+
/** True when a step is a DB-verification step (used to wire the `db` import). */
|
|
25
|
+
export function isDbStep(text: string): boolean {
|
|
26
|
+
return reNoRow.test(text) || reRow.test(text) || reCount.test(text);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
|
|
30
|
+
function valueExpr(token: string): string {
|
|
31
|
+
const t = token.trim();
|
|
32
|
+
const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
33
|
+
if (v) return `testData.get(${JSON.stringify(v[1])})`;
|
|
34
|
+
const q = t.match(/^["'](.*)["']$/);
|
|
35
|
+
if (q) return JSON.stringify(q[1]);
|
|
36
|
+
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
|
|
37
|
+
return JSON.stringify(t);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Parse a `[col] is VALUE [and [col2] is VALUE2]` segment into a JS object literal. */
|
|
41
|
+
function parseFilter(segment: string): string {
|
|
42
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s+is\\s+(${VALUE})`, 'gi');
|
|
43
|
+
const parts: string[] = [];
|
|
44
|
+
let m: RegExpExecArray | null;
|
|
45
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
46
|
+
return `{ ${parts.join(', ')} }`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Parse a `has [col] = VALUE [and [col2] = VALUE2]` segment into a JS object literal. */
|
|
50
|
+
function parseExpected(segment: string): string {
|
|
51
|
+
const re = new RegExp(`\\[([A-Za-z_][A-Za-z0-9_]*)\\]\\s*=\\s*(${VALUE})`, 'gi');
|
|
52
|
+
const parts: string[] = [];
|
|
53
|
+
let m: RegExpExecArray | null;
|
|
54
|
+
while ((m = re.exec(segment))) parts.push(`${JSON.stringify(m[1])}: ${valueExpr(m[2])}`);
|
|
55
|
+
return parts.length ? `{ ${parts.join(', ')} }` : '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const databasePatterns: StepPattern[] = [
|
|
59
|
+
{
|
|
60
|
+
name: 'db-no-row',
|
|
61
|
+
priority: 60, // above generic see-assertions
|
|
62
|
+
matcher: (step: ParsedStep) => reNoRow.test(step.text),
|
|
63
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
64
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+no\\s+row\\s+where\\s+(.+)$`, 'i'))!;
|
|
65
|
+
const table = m[1];
|
|
66
|
+
const filter = parseFilter(m[2]);
|
|
67
|
+
return { code: `await db.assertNoRow(${JSON.stringify(table)}, ${filter});`, comment: `DB: no row in ${table}` };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'db-count',
|
|
72
|
+
priority: 60,
|
|
73
|
+
matcher: (step: ParsedStep) => reCount.test(step.text) && !reRow.test(step.text),
|
|
74
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
75
|
+
const m = step.text.match(new RegExp(`${TABLE}(?:\\s+where\\s+(.+?))?\\s+count\\s+is\\s+(${VALUE})`, 'i'))!;
|
|
76
|
+
const table = m[1];
|
|
77
|
+
const filter = m[2] ? parseFilter(m[2]) : '{}';
|
|
78
|
+
return { code: `await db.assertCount(${JSON.stringify(table)}, ${filter}, Number(${valueExpr(m[3])}));`, comment: `DB: count rows in ${table}` };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'db-row',
|
|
83
|
+
priority: 60,
|
|
84
|
+
matcher: (step: ParsedStep) => reRow.test(step.text),
|
|
85
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
86
|
+
// [table] row where <filter> [has <expected>]
|
|
87
|
+
const m = step.text.match(new RegExp(`${TABLE}\\s+row\\s+where\\s+(.+?)(?:\\s+has\\s+(.+))?$`, 'i'))!;
|
|
88
|
+
const table = m[1];
|
|
89
|
+
const filter = parseFilter(m[2]);
|
|
90
|
+
const expected = m[3] ? parseExpected(m[3]) : '';
|
|
91
|
+
const args = expected ? `${JSON.stringify(table)}, ${filter}, ${expected}` : `${JSON.stringify(table)}, ${filter}`;
|
|
92
|
+
return { code: `await db.assertRow(${args});`, comment: `DB: row in ${table}` };
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
];
|
|
@@ -11,6 +11,7 @@ import { scrollPatterns } from './scroll-patterns';
|
|
|
11
11
|
import { scopePatterns } from './scope-patterns';
|
|
12
12
|
import { tablePatterns } from './table-patterns';
|
|
13
13
|
import { capturePatterns } from './capture-patterns';
|
|
14
|
+
import { databasePatterns } from './database-patterns';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Pattern Registry - manages all step patterns
|
|
@@ -36,6 +37,7 @@ export class PatternRegistry {
|
|
|
36
37
|
this.patterns.push(...scopePatterns);
|
|
37
38
|
this.patterns.push(...tablePatterns);
|
|
38
39
|
this.patterns.push(...capturePatterns);
|
|
40
|
+
this.patterns.push(...databasePatterns);
|
|
39
41
|
|
|
40
42
|
// Sort by priority (higher first)
|
|
41
43
|
this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
@@ -165,4 +167,5 @@ export { keyboardPatterns } from './keyboard-patterns';
|
|
|
165
167
|
export { scrollPatterns } from './scroll-patterns';
|
|
166
168
|
export { scopePatterns } from './scope-patterns';
|
|
167
169
|
export { tablePatterns } from './table-patterns';
|
|
170
|
+
export { databasePatterns, isDbStep } from './database-patterns';
|
|
168
171
|
export * from './types';
|
|
@@ -229,8 +229,8 @@ export class TemplateEngine {
|
|
|
229
229
|
this.baseContext = {};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport });
|
|
232
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
233
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
renderTestFile(data: {
|
package/src/harness/audit.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The score is INTENTIONALLY weighted toward business-critical coverage/depth
|
|
6
6
|
* (not breadth), so it surfaces the gaps a count-based view hides. See
|
|
7
|
-
* docs/orchestration-spec.md §5 and
|
|
7
|
+
* docs/orchestration-spec.md §5 and docs/spec/sungen_refactor_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as path from 'path';
|
|
10
|
+
import * as fs from 'fs';
|
|
10
11
|
import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
11
12
|
import {
|
|
12
13
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
@@ -14,6 +15,10 @@ import {
|
|
|
14
15
|
} from './sensors';
|
|
15
16
|
import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
|
|
16
17
|
import { getProvenance, Provenance } from './provenance';
|
|
18
|
+
import { specCoverage, SpecCoverageResult, parseSpecClauses } from './spec-coverage';
|
|
19
|
+
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
20
|
+
negativeSideEffect, sourceBacked, crossArtifactOwnership } from './quality-gates';
|
|
21
|
+
import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-ledger';
|
|
17
22
|
|
|
18
23
|
export interface AuditReport {
|
|
19
24
|
screen: string;
|
|
@@ -25,6 +30,15 @@ export interface AuditReport {
|
|
|
25
30
|
balance: BalanceResult;
|
|
26
31
|
duplicates: DuplicateResult;
|
|
27
32
|
trace: TraceResult;
|
|
33
|
+
taxonomyMismatch: boolean; // scenarios use IDs not in the project's test-viewpoint.md
|
|
34
|
+
downstream: DownstreamResult; // downstream screens referenced but under-covered
|
|
35
|
+
manualOracle: ManualOracleResult; // @manual scenarios lacking setup/action/oracle
|
|
36
|
+
ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
|
|
37
|
+
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
38
|
+
axes: Record<string, number>;
|
|
39
|
+
weakest: { axis: string; value: number };
|
|
40
|
+
inflated: boolean;
|
|
41
|
+
};
|
|
28
42
|
score: {
|
|
29
43
|
overall: number; // 0..10, business-weighted
|
|
30
44
|
coverage: number; // 0..1
|
|
@@ -37,15 +51,20 @@ export interface AuditReport {
|
|
|
37
51
|
findings: string[]; // human-actionable, what the Repair loop would target
|
|
38
52
|
intent: IntentProfile; // P3 — the intent profile that drove the thresholds
|
|
39
53
|
provenance: Provenance; // D1 — sungen version + catalog hash (diagnose cross-user score gaps)
|
|
54
|
+
spec: SpecCoverageResult; // G2 — spec-clause coverage (FR + validation-trigger matrix)
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
43
58
|
const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
|
|
44
59
|
const viewpointPath = path.join(screenDir, 'requirements', 'test-viewpoint.md');
|
|
45
60
|
|
|
61
|
+
const specPath = path.join(screenDir, 'requirements', 'spec.md');
|
|
62
|
+
const featureText = fs.existsSync(featurePath) ? fs.readFileSync(featurePath, 'utf-8') : '';
|
|
63
|
+
|
|
46
64
|
const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
|
|
47
65
|
const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
|
|
48
66
|
const catalog = loadCatalog();
|
|
67
|
+
const spec = specCoverage(specPath, scenarios, featureText);
|
|
49
68
|
|
|
50
69
|
const gate = viewpointGate(scenarios, viewpoints, catalog);
|
|
51
70
|
// P3 — intent profile from qa/context.md drives the depth threshold (focus).
|
|
@@ -56,6 +75,15 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
56
75
|
const balance = coverageBalance(scenarios);
|
|
57
76
|
const duplicates = duplicateClusters(scenarios);
|
|
58
77
|
const trace = traceability(scenarios, viewpoints);
|
|
78
|
+
// #1 taxonomy-match: when the project defines a viewpoint taxonomy, scenarios must use it.
|
|
79
|
+
const taxonomyMismatch = viewpoints.length > 0 && trace.withVpCode > 0 && trace.mappedRatio < 0.6;
|
|
80
|
+
// #2 downstream-scope + #4 manual-oracle
|
|
81
|
+
const downstream = downstreamScope(readText(specPath), scenarios);
|
|
82
|
+
const manualOracleResult = manualOracle(featureText);
|
|
83
|
+
const ledger = viewpointLedger(viewpointPath, scenarios, featureText);
|
|
84
|
+
const negSideEffect = negativeSideEffect(scenarios);
|
|
85
|
+
const ownership = crossArtifactOwnership(screenDir, scenarios);
|
|
86
|
+
const unsourced = sourceBacked(scenarios, parseSpecClauses(specPath).frs.map((f) => f.id), parseViewpointItems(viewpointPath).map((i) => i.text), viewpoints.map((v) => v.id), featureText);
|
|
59
87
|
|
|
60
88
|
// Sub-scores
|
|
61
89
|
const coverage = gate.coverageRatio;
|
|
@@ -100,16 +128,65 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
100
128
|
if (gate.universalGaps.length) {
|
|
101
129
|
findings.push(`UNIVERSAL: missing theme(s): ${gate.universalGaps.join(', ')} (low priority reminder).`);
|
|
102
130
|
}
|
|
131
|
+
for (const g of spec.triggerGaps) {
|
|
132
|
+
findings.push(`TRIGGER-UNCOVERED: spec validates "${g.constraint}"${g.code ? ` (${g.code})` : ''} on [${g.required.join(', ')}] but scenarios only exercise it on [${g.found.join(', ') || 'none'}] → add a ${g.missing.join(', ')}-trigger scenario for this constraint (don't collapse the trigger × input matrix).`);
|
|
133
|
+
}
|
|
134
|
+
for (const u of spec.uncoveredMust) {
|
|
135
|
+
findings.push(`SPEC-UNCOVERED: ${u.id} (MUST) has no covering scenario — "${u.text}" → add a scenario or tag one @spec:${u.id}.`);
|
|
136
|
+
}
|
|
137
|
+
if (taxonomyMismatch) {
|
|
138
|
+
findings.push(`VP-TAXONOMY-MISMATCH: only ${(trace.mappedRatio * 100).toFixed(0)}% of scenarios use the viewpoint IDs declared in test-viewpoint.md — scenarios invented a generic VP-<CAT> scheme. Re-tag to the project's viewpoint IDs so the coverage matrix is accurate.`);
|
|
139
|
+
}
|
|
140
|
+
for (const d of downstream.underCovered) {
|
|
141
|
+
findings.push(`DOWNSTREAM-SCOPE-MISSING: "${d.route}" is a navigation target but is covered only by a page-nav assertion — cover its content/guards, or scaffold it (\`sungen add --screen ${d.slug}\`).`);
|
|
142
|
+
}
|
|
143
|
+
for (const m of manualOracleResult.insufficient.slice(0, 8)) {
|
|
144
|
+
findings.push(`MANUAL-STEPS-INSUFFICIENT: "${m}" — a @manual scenario needs setup · action · observable expected · oracle/tool (not just a one-line note).`);
|
|
145
|
+
}
|
|
146
|
+
if (ledger.hasViewpoint && ledger.missing.length) {
|
|
147
|
+
const sample = ledger.missing.slice(0, 6).map((m) => m.id || `"${m.text}"`).join(', ');
|
|
148
|
+
findings.push(`VIEWPOINT-ITEM-MISSING: ${ledger.missing.length}/${ledger.total} atomic viewpoint items have no covering scenario (${(ledger.ratio * 100).toFixed(0)}% covered) — e.g. ${sample}. Cover each item or mark it deferred/spec-gap.`);
|
|
149
|
+
}
|
|
150
|
+
for (const n of negSideEffect.slice(0, 6)) {
|
|
151
|
+
findings.push(`NEGATIVE-SIDE-EFFECT-UNPROVEN: "${n}" — the title claims something must NOT happen but the steps don't prove the absence (assert a count / negative state, or make it @manual with an oracle).`);
|
|
152
|
+
}
|
|
153
|
+
for (const d of ownership.duplicates.slice(0, 6)) {
|
|
154
|
+
findings.push(`DUPLICATE-FLOW-OWNERSHIP: "${d.scenario}" has the same shape as a scenario in flow "${d.flow}" — keep one owner (screen-local vs flow); the other should only reference/set up.`);
|
|
155
|
+
}
|
|
156
|
+
for (const u of unsourced.slice(0, 6)) {
|
|
157
|
+
findings.push(`UNSOURCEABLE-SCENARIO: "${u}" doesn't trace to any FR / viewpoint item — link it to a source, or tag it @exploration (not part of the official suite).`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// #8 — multi-axis calibration: a high overall must not hide a weak axis.
|
|
161
|
+
const manualCompleteness = manualOracleResult.manualTotal
|
|
162
|
+
? 1 - manualOracleResult.insufficient.length / manualOracleResult.manualTotal : 1;
|
|
163
|
+
const axes: Record<string, number> = {
|
|
164
|
+
coverage: Math.round(coverage * 100) / 100,
|
|
165
|
+
businessDepth: Math.round(businessDepth * 100) / 100,
|
|
166
|
+
claimProof: Math.round(claim.ratio * 100) / 100,
|
|
167
|
+
specFR: spec.frTotal ? Math.round((spec.frCovered / spec.frTotal) * 100) / 100 : 1,
|
|
168
|
+
atomicLedger: Math.round(ledger.ratio * 100) / 100,
|
|
169
|
+
manualOracle: Math.round(manualCompleteness * 100) / 100,
|
|
170
|
+
taxonomy: taxonomyMismatch ? 0 : Math.round(trace.mappedRatio * 100) / 100,
|
|
171
|
+
};
|
|
172
|
+
const weakestEntry = Object.entries(axes).sort((a, b) => a[1] - b[1])[0];
|
|
173
|
+
const weakest = { axis: weakestEntry[0], value: weakestEntry[1] };
|
|
174
|
+
const inflated = overall >= 8 && weakest.value < 0.6;
|
|
175
|
+
if (inflated) {
|
|
176
|
+
findings.push(`SCORE-INFLATED-BY-BREADTH: overall ${Math.round(overall * 10) / 10}/10 but the weakest axis "${weakest.axis}" is ${(weakest.value * 100).toFixed(0)}% — breadth is hiding a weak dimension. Raise "${weakest.axis}" before trusting the headline.`);
|
|
177
|
+
}
|
|
178
|
+
const calibration = { axes, weakest, inflated };
|
|
103
179
|
|
|
104
|
-
// Gate
|
|
105
|
-
//
|
|
180
|
+
// Gate spans coverage (viewpoint themes), depth, claim-proof, spec-clause coverage,
|
|
181
|
+
// AND taxonomy-match (scenarios must use the project's viewpoint IDs when defined).
|
|
106
182
|
const gateStatus: 'PASS' | 'FAIL' =
|
|
107
|
-
gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' ? 'PASS' : 'FAIL';
|
|
183
|
+
gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' && spec.verdict !== 'fail' && !taxonomyMismatch ? 'PASS' : 'FAIL';
|
|
108
184
|
|
|
109
185
|
return {
|
|
110
186
|
screen: screenName,
|
|
111
187
|
scenarioCount: scenarios.length,
|
|
112
|
-
gate, depth, claim, taxonomy, balance, duplicates, trace,
|
|
188
|
+
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
189
|
+
taxonomyMismatch, downstream, manualOracle: manualOracleResult, ledger, calibration,
|
|
113
190
|
score: {
|
|
114
191
|
overall: Math.round(overall * 10) / 10,
|
|
115
192
|
coverage: Math.round(coverage * 100) / 100,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Classifies each scenario's execution mode + each @manual case by reason code
|
|
5
5
|
* (M1–M9), maps capability-reasons to drivers, and emits the manual-reason KPI.
|
|
6
6
|
* Never installs anything (that's `sungen capability add`). See
|
|
7
|
-
*
|
|
7
|
+
* docs/spec/sungen_phase2b_spec.md.
|
|
8
8
|
*/
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
@@ -43,6 +43,17 @@ const INFER: { code: string; re: RegExp }[] = [
|
|
|
43
43
|
{ code: 'M9', re: /\b(judgment|human|subjective|manual review)\b/i },
|
|
44
44
|
];
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Classify free text (e.g. a legacy testcase's precondition+steps+expected) into a
|
|
48
|
+
* manual-reason code, or '' when nothing matches (→ UI-automatable). Reuses the same
|
|
49
|
+
* INFER patterns as the scenario planner so legacy-ingest and the Gherkin planner agree.
|
|
50
|
+
*/
|
|
51
|
+
export function classifyReason(text: string): string {
|
|
52
|
+
const t = (text || '').toLowerCase();
|
|
53
|
+
for (const { code, re } of INFER) if (re.test(t)) return code;
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason: string }
|
|
47
58
|
|
|
48
59
|
/** Parse scenarios with their tags + the reason comment line above (for @manual). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Driver Catalog (metadata only — NO driver code is bundled here).
|
|
2
2
|
# Lets Sungen RECOMMEND/RESOLVE a driver that may not be installed yet, and tells
|
|
3
|
-
# `sungen capability add` which package to install. See
|
|
3
|
+
# `sungen capability add` which package to install. See docs/spec/sungen_phase2a_spec.md.
|
|
4
4
|
#
|
|
5
5
|
# kind: platform → the runtime/codegen adapter for a target (pick ONE per project)
|
|
6
6
|
# kind: capability → an extra ability added on top of a platform (Phase 3)
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Each page-type lists must-cover themes. A theme is "covered" when the project's
|
|
7
7
|
# viewpoint-overview (or generated scenarios) contains one of its keywords.
|
|
8
|
-
# See docs/orchestration-spec.md §5.2 and
|
|
8
|
+
# See docs/orchestration-spec.md §5.2 and docs/spec/sungen_refactor_spec.md §9.
|
|
9
9
|
#
|
|
10
10
|
# `depth:` (optional, harness-roadmap P1) marks a theme as DATA-correctness:
|
|
11
11
|
# requires: data-assertion → scenarios on this theme must assert DATA (not just
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static skill-lint (Eval Harness L1) — deterministic quality checks on Sungen's OWN
|
|
3
|
+
* AI-instruction templates, so a broken / unregistered / oversized skill fails before it
|
|
4
|
+
* ships. Learned (generically) from the "static validations" tier of an agent-kit evals
|
|
5
|
+
* layer. No project data — this lints the sungen package's own templates.
|
|
6
|
+
*
|
|
7
|
+
* Design note: the checks are MAPPING-DRIVEN. `AI_RULES_FILE_MAPPING` is the source of
|
|
8
|
+
* truth for what each template installs as, so the lint uses the install target (does it
|
|
9
|
+
* end in `/SKILL.md`?) to tell a top-level skill from a sub-content fragment — instead of
|
|
10
|
+
* guessing from filenames. We deliberately do NOT enforce claude↔github body parity: the
|
|
11
|
+
* two variants are hand-tuned per platform and intentionally diverge in wording and even
|
|
12
|
+
* structure, so byte/heading equality would be pure false positives.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import { AI_RULES_FILE_MAPPING } from '../../orchestrator/ai-rules-updater';
|
|
17
|
+
|
|
18
|
+
export interface SkillLintFinding { level: 'error' | 'warn'; file: string; rule: string; detail: string }
|
|
19
|
+
export interface SkillLintResult { checked: number; findings: SkillLintFinding[]; errors: number }
|
|
20
|
+
|
|
21
|
+
const LINE_BUDGET = 700; // a skill much larger than this is a context-cost smell (warn)
|
|
22
|
+
const SKILL_RE = /^(claude|github)-skill-/;
|
|
23
|
+
|
|
24
|
+
function stripFrontmatter(text: string): { fm: string | null; body: string } {
|
|
25
|
+
const m = text.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
26
|
+
if (!m) return { fm: null, body: text };
|
|
27
|
+
return { fm: m[1], body: text.slice(m[0].length) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Lint the AI-instruction templates in `dir` (default: the sungen source templates). */
|
|
31
|
+
export function lintSkills(dir: string): SkillLintResult {
|
|
32
|
+
const findings: SkillLintFinding[] = [];
|
|
33
|
+
const files = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')) : [];
|
|
34
|
+
const skillFiles = files.filter((f) => SKILL_RE.test(f));
|
|
35
|
+
|
|
36
|
+
// mapping: template file -> install target (source of truth for "is this a top-level skill")
|
|
37
|
+
const target = new Map<string, string>(AI_RULES_FILE_MAPPING.map(([tpl, dst]) => [tpl, dst]));
|
|
38
|
+
const isTopLevelSkill = (f: string) => (target.get(f) || '').endsWith('/SKILL.md');
|
|
39
|
+
|
|
40
|
+
// 1) registration integrity (bidirectional) — the highest-value check:
|
|
41
|
+
// a skill file missing from the mapping never installs; a mapping to a missing file
|
|
42
|
+
// ships a broken/empty skill.
|
|
43
|
+
for (const f of skillFiles) {
|
|
44
|
+
if (!target.has(f)) findings.push({ level: 'error', file: f, rule: 'unregistered', detail: 'skill template not in AI_RULES_FILE_MAPPING (it would never be installed)' });
|
|
45
|
+
}
|
|
46
|
+
for (const [tpl] of AI_RULES_FILE_MAPPING) {
|
|
47
|
+
if (!fs.existsSync(path.join(dir, tpl))) findings.push({ level: 'error', file: tpl, rule: 'mapped-missing', detail: 'AI_RULES_FILE_MAPPING points to a template that does not exist' });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2) frontmatter (name + description) — ONLY for top-level skills (SKILL.md targets).
|
|
51
|
+
// Sub-content fragments (mode-*.md, group-*.md) are loaded by their parent router
|
|
52
|
+
// and legitimately carry no frontmatter.
|
|
53
|
+
for (const f of skillFiles) {
|
|
54
|
+
if (!isTopLevelSkill(f)) continue;
|
|
55
|
+
const text = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
56
|
+
const { fm } = stripFrontmatter(text);
|
|
57
|
+
if (!fm) { findings.push({ level: 'error', file: f, rule: 'frontmatter', detail: 'top-level skill (SKILL.md) is missing --- frontmatter --- (Claude/Copilot will not load it)' }); continue; }
|
|
58
|
+
if (!/\bname\s*:/.test(fm)) findings.push({ level: 'error', file: f, rule: 'frontmatter-name', detail: 'no `name:` in frontmatter' });
|
|
59
|
+
if (!/\bdescription\s*:/.test(fm)) findings.push({ level: 'error', file: f, rule: 'frontmatter-description', detail: 'no `description:` in frontmatter' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3) line budget — context-cost smell (advisory).
|
|
63
|
+
for (const f of skillFiles) {
|
|
64
|
+
const lines = fs.readFileSync(path.join(dir, f), 'utf8').split('\n').length;
|
|
65
|
+
if (lines > LINE_BUDGET) findings.push({ level: 'warn', file: f, rule: 'line-budget', detail: `${lines} lines > ${LINE_BUDGET} (context-cost smell)` });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4) variant PRESENCE (not body equality) — every top-level skill should ship for both
|
|
69
|
+
// platforms. Catches "added a Claude skill but forgot the Copilot variant". Advisory.
|
|
70
|
+
const skillName = (dst: string) => { const m = dst.match(/\/(sungen-[^/]+)\/SKILL\.md$/); return m ? m[1] : null; };
|
|
71
|
+
const claudeSkills = new Set<string>(), githubSkills = new Set<string>();
|
|
72
|
+
for (const f of skillFiles) {
|
|
73
|
+
if (!isTopLevelSkill(f)) continue;
|
|
74
|
+
const name = skillName(target.get(f)!); if (!name) continue;
|
|
75
|
+
(f.startsWith('claude-') ? claudeSkills : githubSkills).add(name);
|
|
76
|
+
}
|
|
77
|
+
for (const n of claudeSkills) if (!githubSkills.has(n)) findings.push({ level: 'warn', file: `claude .../${n}/SKILL.md`, rule: 'variant-missing', detail: `Claude skill "${n}" has no GitHub (Copilot) variant` });
|
|
78
|
+
for (const n of githubSkills) if (!claudeSkills.has(n)) findings.push({ level: 'warn', file: `github .../${n}/SKILL.md`, rule: 'variant-missing', detail: `GitHub skill "${n}" has no Claude variant` });
|
|
79
|
+
|
|
80
|
+
return { checked: skillFiles.length, findings, errors: findings.filter((f) => f.level === 'error').length };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Default templates dir, resolved relative to this module (works from src via tsx and dist). */
|
|
84
|
+
export function defaultSkillDir(): string {
|
|
85
|
+
// src/harness/eval → src/orchestrator/... | dist/harness/eval → dist/orchestrator/...
|
|
86
|
+
return path.resolve(__dirname, '..', '..', 'orchestrator', 'templates', 'ai-instructions');
|
|
87
|
+
}
|
package/src/harness/flow-plan.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* leg's SELECTOR READINESS + capability, folds in the manual-reason taxonomy
|
|
6
6
|
* (capability-plan) and the run-test contract (flow-check), and emits a run-test
|
|
7
7
|
* PLAN. Automates the manual diagnosis done while healing cart-and-filter.
|
|
8
|
-
* See
|
|
8
|
+
* See docs/spec/sungen_phase2c_spec.md.
|
|
9
9
|
*/
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
package/src/harness/parse.ts
CHANGED
|
@@ -29,6 +29,18 @@ export interface ScenarioInfo {
|
|
|
29
29
|
stepSkeleton: string; // normalized steps for duplicate clustering
|
|
30
30
|
haystack: string; // lowercase name + steps text (for keyword coverage)
|
|
31
31
|
stepsText: string; // lowercase steps ONLY (name excluded) — for claim-proof
|
|
32
|
+
vpId?: string; // raw leading ID token of the title (project's scheme: VP0-001, MS-HP-001, VP-LIST-001)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Format-tolerant: is this token an ID (project's scheme), not a prose word?
|
|
36
|
+
* Accepts VP0, VP0-001, MS-HP-001, TV-01, VP-LIST-001 — requires a digit + uppercase start. */
|
|
37
|
+
export function isIdLike(s: string): boolean {
|
|
38
|
+
return /^[A-Z][A-Za-z0-9.-]*$/.test(s) && /\d/.test(s) && s.length >= 3;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The ID minus its trailing -NNN sequence number (VP0-001 → VP0, MS-HP-001 → MS-HP). */
|
|
42
|
+
export function idPrefix(id: string): string {
|
|
43
|
+
return id.replace(/[-.]\d{1,4}$/, '');
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
// ---------- test-viewpoint.md ----------
|
|
@@ -50,7 +62,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
50
62
|
const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
51
63
|
if (cells.length >= 3) {
|
|
52
64
|
const id = cells[0];
|
|
53
|
-
if (
|
|
65
|
+
if (isIdLike(id) && !/^-+$/.test(cells[1])) {
|
|
54
66
|
const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
|
|
55
67
|
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
56
68
|
}
|
|
@@ -66,8 +78,8 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
66
78
|
if (g) { group = (g[1][0].toUpperCase() + g[1].slice(1).toLowerCase()) as any; continue; }
|
|
67
79
|
if (/^##\s/.test(line)) { group = undefined; }
|
|
68
80
|
if (group) {
|
|
69
|
-
const m = line.match(
|
|
70
|
-
if (m) {
|
|
81
|
+
const m = line.match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
|
|
82
|
+
if (m && isIdLike(m[1])) {
|
|
71
83
|
const id = m[1].toUpperCase();
|
|
72
84
|
const existing = entries.get(id);
|
|
73
85
|
if (existing) existing.group = group;
|
|
@@ -92,6 +104,9 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
92
104
|
const codeMatch = sc.name.match(/\bVP-([A-Z]+)-\d+/i);
|
|
93
105
|
const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
|
|
94
106
|
const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
|
|
107
|
+
// Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
|
|
108
|
+
const leadMatch = sc.name.match(/^\s*([A-Za-z][A-Za-z0-9.-]*)/);
|
|
109
|
+
const vpId = leadMatch && isIdLike(leadMatch[1]) ? leadMatch[1].toUpperCase() : undefined;
|
|
95
110
|
|
|
96
111
|
// Then-phase detection (And/But inherit previous primary keyword)
|
|
97
112
|
let last = 'Given';
|
|
@@ -136,6 +151,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
136
151
|
stepSkeleton: skeletonParts.join(' | '),
|
|
137
152
|
haystack: textParts.join(' ').toLowerCase(),
|
|
138
153
|
stepsText: stepTextParts.join(' ').toLowerCase(),
|
|
154
|
+
vpId,
|
|
139
155
|
};
|
|
140
156
|
}
|
|
141
157
|
|