@sun-asterisk/sungen 3.0.0-beta.83 → 3.0.0-beta.92

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 (46) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +0 -14
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/index.js +0 -2
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/harness/audit.d.ts +0 -14
  7. package/dist/harness/audit.d.ts.map +1 -1
  8. package/dist/harness/audit.js +3 -56
  9. package/dist/harness/audit.js.map +1 -1
  10. package/dist/harness/parse.d.ts +0 -6
  11. package/dist/harness/parse.d.ts.map +1 -1
  12. package/dist/harness/parse.js +3 -18
  13. package/dist/harness/parse.js.map +1 -1
  14. package/dist/harness/sensors.d.ts.map +1 -1
  15. package/dist/harness/sensors.js +5 -53
  16. package/dist/harness/sensors.js.map +1 -1
  17. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +1 -24
  18. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +7 -43
  19. package/package.json +2 -2
  20. package/src/cli/commands/audit.ts +0 -12
  21. package/src/cli/index.ts +0 -2
  22. package/src/harness/audit.ts +4 -68
  23. package/src/harness/parse.ts +3 -19
  24. package/src/harness/sensors.ts +6 -52
  25. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +1 -24
  26. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +7 -43
  27. package/dist/cli/commands/eval.d.ts +0 -3
  28. package/dist/cli/commands/eval.d.ts.map +0 -1
  29. package/dist/cli/commands/eval.js +0 -37
  30. package/dist/cli/commands/eval.js.map +0 -1
  31. package/dist/harness/eval/skill-lint.d.ts +0 -16
  32. package/dist/harness/eval/skill-lint.d.ts.map +0 -1
  33. package/dist/harness/eval/skill-lint.js +0 -129
  34. package/dist/harness/eval/skill-lint.js.map +0 -1
  35. package/dist/harness/quality-gates.d.ts +0 -29
  36. package/dist/harness/quality-gates.d.ts.map +0 -1
  37. package/dist/harness/quality-gates.js +0 -183
  38. package/dist/harness/quality-gates.js.map +0 -1
  39. package/dist/harness/viewpoint-ledger.d.ts +0 -23
  40. package/dist/harness/viewpoint-ledger.d.ts.map +0 -1
  41. package/dist/harness/viewpoint-ledger.js +0 -118
  42. package/dist/harness/viewpoint-ledger.js.map +0 -1
  43. package/src/cli/commands/eval.ts +0 -28
  44. package/src/harness/eval/skill-lint.ts +0 -87
  45. package/src/harness/quality-gates.ts +0 -152
  46. package/src/harness/viewpoint-ledger.ts +0 -80
@@ -1,152 +0,0 @@
1
- /**
2
- * Quality gates (batch): downstream-scope + manual-oracle + negative-side-effect +
3
- * cross-artifact ownership + source-backed strictness.
4
- * Generic — read the project's own spec.md / feature text / sibling flows; no project data.
5
- */
6
- import * as fs from 'fs';
7
- import * as path from 'path';
8
- import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
9
-
10
- // ---------- #2 Downstream-scope ----------
11
-
12
- export interface DownstreamResult {
13
- downstreamRoutes: string[]; // success/navigation targets ≠ own route
14
- underCovered: { route: string; slug: string }[]; // referenced only by a bare page-nav
15
- }
16
-
17
- /** Routes the spec hands off to (Navigation Flow / success), other than the screen's own route. */
18
- function downstreamRoutes(specText: string): string[] {
19
- const ownRoute = (specText.match(/\*\*Route\*\*\s*:\s*`?(\/[^\s`]+)/) || [])[1] || '';
20
- const routes = new Set<string>();
21
- for (const line of specText.split('\n')) {
22
- if (!/success|navigat|to \(|→/i.test(line)) continue;
23
- for (const m of line.matchAll(/`?(\/[a-z][a-z0-9/_-]+)`?/gi)) {
24
- const r = m[1];
25
- if (r !== ownRoute && r.split('/').length > ownRoute.split('/').length - 0) routes.add(r);
26
- }
27
- }
28
- // keep only routes that extend beyond the own route (a distinct downstream surface)
29
- return [...routes].filter((r) => r !== ownRoute && (!ownRoute || r.startsWith(ownRoute + '/') || r.split('/').length >= 3));
30
- }
31
-
32
- export function downstreamScope(specText: string, scenarios: ScenarioInfo[]): DownstreamResult {
33
- const routes = downstreamRoutes(specText);
34
- const underCovered: { route: string; slug: string }[] = [];
35
- for (const route of routes) {
36
- const slug = (route.split('/').filter(Boolean).pop() || route).toLowerCase();
37
- const refs = scenarios.filter((s) => s.haystack.includes(slug) || s.haystack.includes(route.toLowerCase()));
38
- if (!refs.length) continue; // not referenced at all — out of this screen's scope entirely
39
- // Substantively covered only if some scenario OPERATES on the downstream — i.e. it
40
- // starts there (`is on [<downstream>]`) — not merely navigates to it as a terminal
41
- // `see [<downstream>] page` assertion. The latter just proves the transition.
42
- const opensOn = new RegExp(`\\bis on \\[[^\\]]*${slug}`, 'i');
43
- const contentCovered = refs.some((s) => opensOn.test(s.haystack));
44
- if (!contentCovered) underCovered.push({ route, slug });
45
- }
46
- return { downstreamRoutes: routes, underCovered };
47
- }
48
-
49
- // ---------- #4 Manual-oracle ----------
50
-
51
- export interface ManualOracleResult {
52
- manualTotal: number;
53
- insufficient: string[]; // @manual scenarios lacking setup/action/oracle
54
- }
55
-
56
- function blocks(featureText: string): string[] {
57
- return featureText.split(/\n\s*\n/).filter((b) => /\bScenario:/.test(b));
58
- }
59
-
60
- export function manualOracle(featureText: string): ManualOracleResult {
61
- const insufficient: string[] = [];
62
- let manualTotal = 0;
63
- for (const b of blocks(featureText)) {
64
- if (!/@manual\b/.test(b)) continue;
65
- manualTotal++;
66
- const commentLines = b.split('\n').filter((l) => /^\s*#/.test(l));
67
- const hasOracle = /tester verifies|oracle\s*:|requires|verify that|expected\s*:|steps?\s*:/i.test(b);
68
- const hasNumberedSteps = /^\s*#?\s*\d+\.\s/m.test(b);
69
- // sufficient = an oracle/steps marker, OR a substantive comment block (≥3 comment lines)
70
- if (!(hasOracle || hasNumberedSteps || commentLines.length >= 3)) {
71
- const name = (b.match(/Scenario:\s*(.+)/) || [])[1] || '(unnamed)';
72
- insufficient.push(name.trim().slice(0, 80));
73
- }
74
- }
75
- return { manualTotal, insufficient };
76
- }
77
-
78
- // ---------- #4 Negative side-effect ----------
79
-
80
- const NEG_TITLE = /\b(does not|doesn't|no second|not dispatch|not sent|without submitting|no leak|single request|exactly one|count is 1|only one request|no duplicate|not create)\b/i;
81
-
82
- /** Titles asserting an ABSENCE must prove it (count / negative / @manual+oracle), not just a happy outcome. */
83
- export function negativeSideEffect(scenarios: ScenarioInfo[]): string[] {
84
- const flagged: string[] = [];
85
- for (const s of scenarios) {
86
- if (s.manual) continue; // @manual is a legitimate deferral (oracle checked by #4 manual-oracle)
87
- if (!NEG_TITLE.test(s.name)) continue;
88
- const proven = /\bcount\b|tohavecount|table with|is hidden|are hidden|not complete|message is hidden/.test(s.stepsText);
89
- if (!proven) flagged.push(s.name.slice(0, 80));
90
- }
91
- return flagged;
92
- }
93
-
94
- // ---------- #7 Source-backed strictness ----------
95
-
96
- /** A scenario should trace to a source: a viewpoint ID (its own scheme), an FR id, or a
97
- * viewpoint item (keyword overlap). ID match is language-agnostic and primary. */
98
- export function sourceBacked(scenarios: ScenarioInfo[], frIds: string[], viewpointItems: string[], viewpointIds: string[], featureText: string): string[] {
99
- if (!frIds.length && !viewpointItems.length && !viewpointIds.length) return []; // no contract
100
- const vpIds = viewpointIds.map((s) => s.toUpperCase());
101
- const itemWords = viewpointItems.map((t) => new Set((t.toLowerCase().match(/[a-z][a-z-]{4,}/g) || [])));
102
- // per-scenario blocks (INCLUDING comments) so an FR cited in a comment counts as a source
103
- const blockOf = new Map<string, string>();
104
- for (const b of featureText.split(/\n\s*\n/)) {
105
- const m = b.match(/Scenario:\s*(.+)/);
106
- if (m) blockOf.set(m[1].trim().toLowerCase(), b.toLowerCase());
107
- }
108
- const unsourced: string[] = [];
109
- for (const s of scenarios) {
110
- const id = (s.vpId || s.vpCode || '').toUpperCase();
111
- const mapsId = !!id && vpIds.some((v) => id === v || id.startsWith(v) || v.startsWith(idPrefix(id)));
112
- const block = blockOf.get(s.name.trim().toLowerCase()) || s.haystack;
113
- const citesFr = frIds.some((fid) => block.includes(fid.toLowerCase()));
114
- const sWords = new Set((s.haystack.match(/[a-z][a-z-]{4,}/g) || []));
115
- const mapsItem = itemWords.some((iw) => { let hits = 0; for (const w of iw) if (sWords.has(w)) hits++; return hits >= 2; });
116
- if (!mapsId && !citesFr && !mapsItem) unsourced.push(s.name.slice(0, 80));
117
- }
118
- return unsourced;
119
- }
120
-
121
- // ---------- #6 Cross-artifact ownership ----------
122
-
123
- export interface OwnershipResult { duplicates: { scenario: string; flow: string }[] }
124
-
125
- /** Scenarios whose step-skeleton also appears in a sibling flow feature → duplicate ownership. */
126
- export function crossArtifactOwnership(screenDir: string, scenarios: ScenarioInfo[]): OwnershipResult {
127
- const duplicates: { scenario: string; flow: string }[] = [];
128
- // screenDir = <root>/qa/screens/<name>; flows live at <root>/qa/flows/*/features/*.feature
129
- const flowsRoot = path.resolve(screenDir, '..', '..', 'flows');
130
- if (!fs.existsSync(flowsRoot)) return { duplicates };
131
- const bySkeleton = new Map<string, string>();
132
- for (const flow of fs.readdirSync(flowsRoot)) {
133
- const fdir = path.join(flowsRoot, flow, 'features');
134
- if (!fs.existsSync(fdir)) continue;
135
- for (const f of fs.readdirSync(fdir).filter((x) => x.endsWith('.feature'))) {
136
- for (const fs2 of loadScenarios(path.join(fdir, f))) {
137
- if (fs2.stepSkeleton && fs2.stepSkeleton.length > 20) bySkeleton.set(fs2.stepSkeleton, flow);
138
- }
139
- }
140
- }
141
- if (!bySkeleton.size) return { duplicates };
142
- for (const s of scenarios) {
143
- const flow = s.stepSkeleton && s.stepSkeleton.length > 20 ? bySkeleton.get(s.stepSkeleton) : undefined;
144
- if (flow) duplicates.push({ scenario: s.name.slice(0, 70), flow });
145
- }
146
- return { duplicates };
147
- }
148
-
149
- // convenience reader
150
- export function readText(p: string): string {
151
- return fs.existsSync(p) ? fs.readFileSync(p, 'utf-8') : '';
152
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Viewpoint Atomic Coverage Ledger (harness #2).
3
- *
4
- * The project's `test-viewpoint.md` IS the coverage contract. This parses it into ATOMIC
5
- * items (each bullet / table row / ID-prefixed line) and reports the status of EACH —
6
- * covered / missing — instead of the coarse "viewpoint mentioned" signal. It is fully
7
- * project-driven (works on any project's viewpoint file, any domain), which is why it
8
- * scales where a hardcoded domain catalog does not. Advisory: it surfaces the per-item
9
- * gaps that inflate a "looks-covered" score; it does not fail the gate.
10
- */
11
- import * as fs from 'fs';
12
- import { ScenarioInfo } from './parse';
13
-
14
- export interface LedgerItem { id?: string; text: string; covered: boolean }
15
-
16
- export interface LedgerResult {
17
- hasViewpoint: boolean;
18
- total: number;
19
- covered: number;
20
- ratio: number;
21
- missing: { id?: string; text: string }[];
22
- }
23
-
24
- const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.Title, VP7-002, MS-HP-001, TV-01
25
- const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
26
-
27
- /** Extract atomic checklist items from a viewpoint file (format-tolerant). */
28
- export function parseViewpointItems(viewpointPath: string): { id?: string; text: string }[] {
29
- if (!fs.existsSync(viewpointPath)) return [];
30
- const lines = fs.readFileSync(viewpointPath, 'utf-8').split('\n');
31
- const items: { id?: string; text: string }[] = [];
32
- let inFence = false;
33
- for (const raw of lines) {
34
- const line = raw.trim();
35
- if (line.startsWith('```')) { inFence = !inFence; continue; }
36
- if (inFence || !line) continue;
37
- if (/^#{1,6}\s/.test(line)) continue; // markdown heading
38
- let text = '';
39
- const bullet = line.match(/^(?:[-*+]|\d+[.)])\s+(.*)$/);
40
- if (bullet) text = bullet[1];
41
- else if (line.startsWith('|')) { // table data row
42
- if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
43
- const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
44
- if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status)$/i.test(cells[0] || '')) continue; // header
45
- text = cells.join(' — ');
46
- } else continue;
47
- text = text.replace(/[*`]/g, '').trim();
48
- if (!text) continue;
49
- const idM = text.match(ID_RE);
50
- const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
51
- const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
52
- if (!id && words.length < 2) continue; // not substantive enough to track
53
- items.push({ id, text: text.slice(0, 100) });
54
- }
55
- return items;
56
- }
57
-
58
- export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[], featureText: string): LedgerResult {
59
- const items = parseViewpointItems(viewpointPath);
60
- if (!fs.existsSync(viewpointPath) || items.length === 0) {
61
- return { hasViewpoint: fs.existsSync(viewpointPath), total: 0, covered: 0, ratio: 1, missing: [] };
62
- }
63
- const featLower = featureText.toLowerCase();
64
- const missing: { id?: string; text: string }[] = [];
65
- let covered = 0;
66
-
67
- for (const item of items) {
68
- let isCovered = false;
69
- if (item.id && featLower.includes(item.id.toLowerCase())) isCovered = true;
70
- else {
71
- const words = [...new Set((item.text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w)))];
72
- const need = Math.min(2, words.length);
73
- isCovered = words.length > 0 && scenarios.some((s) => words.filter((w) => s.haystack.includes(w)).length >= need);
74
- }
75
- if (isCovered) covered++;
76
- else missing.push({ id: item.id, text: item.text });
77
- }
78
-
79
- return { hasViewpoint: true, total: items.length, covered, ratio: items.length ? covered / items.length : 1, missing };
80
- }