@sun-asterisk/sungen 2.7.0-beta.1 → 3.0.0-beta.71
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/README.md +1 -1
- package/dist/cli/commands/add.js +3 -3
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +3 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +134 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/blindspot.d.ts +3 -0
- package/dist/cli/commands/blindspot.d.ts.map +1 -0
- package/dist/cli/commands/blindspot.js +58 -0
- package/dist/cli/commands/blindspot.js.map +1 -0
- package/dist/cli/commands/challenge.d.ts +3 -0
- package/dist/cli/commands/challenge.d.ts.map +1 -0
- package/dist/cli/commands/challenge.js +102 -0
- package/dist/cli/commands/challenge.js.map +1 -0
- package/dist/cli/commands/feedback.d.ts +3 -0
- package/dist/cli/commands/feedback.d.ts.map +1 -0
- package/dist/cli/commands/feedback.js +72 -0
- package/dist/cli/commands/feedback.js.map +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +22 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/ledger.d.ts +3 -0
- package/dist/cli/commands/ledger.d.ts.map +1 -0
- package/dist/cli/commands/ledger.js +71 -0
- package/dist/cli/commands/ledger.js.map +1 -0
- package/dist/cli/commands/manifest.d.ts +3 -0
- package/dist/cli/commands/manifest.d.ts.map +1 -0
- package/dist/cli/commands/manifest.js +101 -0
- package/dist/cli/commands/manifest.js.map +1 -0
- package/dist/cli/commands/script-check.d.ts +3 -0
- package/dist/cli/commands/script-check.d.ts.map +1 -0
- package/dist/cli/commands/script-check.js +97 -0
- package/dist/cli/commands/script-check.js.map +1 -0
- package/dist/cli/commands/trace.d.ts +3 -0
- package/dist/cli/commands/trace.d.ts.map +1 -0
- package/dist/cli/commands/trace.js +110 -0
- package/dist/cli/commands/trace.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +22 -9
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +16 -0
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/capture-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +2 -0
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +1 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +5 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +17 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts +24 -0
- package/dist/harness/audit.d.ts.map +1 -0
- package/dist/harness/audit.js +115 -0
- package/dist/harness/audit.js.map +1 -0
- package/dist/harness/blindspot.d.ts +15 -0
- package/dist/harness/blindspot.d.ts.map +1 -0
- package/dist/harness/blindspot.js +85 -0
- package/dist/harness/blindspot.js.map +1 -0
- package/dist/harness/catalog/universal-viewpoints.yaml +114 -0
- package/dist/harness/challenge.d.ts +21 -0
- package/dist/harness/challenge.d.ts.map +1 -0
- package/dist/harness/challenge.js +151 -0
- package/dist/harness/challenge.js.map +1 -0
- package/dist/harness/feedback.d.ts +29 -0
- package/dist/harness/feedback.d.ts.map +1 -0
- package/dist/harness/feedback.js +106 -0
- package/dist/harness/feedback.js.map +1 -0
- package/dist/harness/intent.d.ts +11 -0
- package/dist/harness/intent.d.ts.map +1 -0
- package/dist/harness/intent.js +86 -0
- package/dist/harness/intent.js.map +1 -0
- package/dist/harness/ledger.d.ts +42 -0
- package/dist/harness/ledger.d.ts.map +1 -0
- package/dist/harness/ledger.js +171 -0
- package/dist/harness/ledger.js.map +1 -0
- package/dist/harness/manifest.d.ts +42 -0
- package/dist/harness/manifest.d.ts.map +1 -0
- package/dist/harness/manifest.js +209 -0
- package/dist/harness/manifest.js.map +1 -0
- package/dist/harness/parse.d.ts +22 -0
- package/dist/harness/parse.d.ts.map +1 -0
- package/dist/harness/parse.js +163 -0
- package/dist/harness/parse.js.map +1 -0
- package/dist/harness/script-check.d.ts +16 -0
- package/dist/harness/script-check.d.ts.map +1 -0
- package/dist/harness/script-check.js +169 -0
- package/dist/harness/script-check.js.map +1 -0
- package/dist/harness/secret-scan.d.ts +8 -0
- package/dist/harness/secret-scan.d.ts.map +1 -0
- package/dist/harness/secret-scan.js +88 -0
- package/dist/harness/secret-scan.js.map +1 -0
- package/dist/harness/sensors.d.ts +88 -0
- package/dist/harness/sensors.d.ts.map +1 -0
- package/dist/harness/sensors.js +232 -0
- package/dist/harness/sensors.js.map +1 -0
- package/dist/harness/trace.d.ts +31 -0
- package/dist/harness/trace.d.ts.map +1 -0
- package/dist/harness/trace.js +173 -0
- package/dist/harness/trace.js.map +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts +1 -0
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +55 -11
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/figma/spec-figma-renderer.d.ts +2 -2
- package/dist/orchestrator/figma/spec-figma-renderer.js +2 -2
- package/dist/orchestrator/figma/spec-figma-section-renderers.d.ts +1 -1
- package/dist/orchestrator/figma/spec-figma-section-renderers.js +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +10 -6
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +1 -4
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
- package/dist/orchestrator/templates/ai-instructions/{github-skill-sungen-figma-source.md → claude-skill-capture-mode-figma-pat.md} +14 -48
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
- package/{src/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
- package/dist/orchestrator/templates/specs-test-data.ts +9 -0
- package/dist/tools/figma/figma-auth.d.ts +5 -2
- package/dist/tools/figma/figma-auth.d.ts.map +1 -1
- package/dist/tools/figma/figma-auth.js +19 -9
- package/dist/tools/figma/figma-auth.js.map +1 -1
- package/docs/orchestration-spec.md +267 -0
- package/package.json +10 -6
- package/src/cli/commands/add.ts +3 -3
- package/src/cli/commands/audit.ts +92 -0
- package/src/cli/commands/blindspot.ts +48 -0
- package/src/cli/commands/challenge.ts +55 -0
- package/src/cli/commands/feedback.ts +65 -0
- package/src/cli/commands/generate.ts +19 -0
- package/src/cli/commands/ledger.ts +61 -0
- package/src/cli/commands/manifest.ts +55 -0
- package/src/cli/commands/script-check.ts +50 -0
- package/src/cli/commands/trace.ts +60 -0
- package/src/cli/commands/update.ts +30 -10
- package/src/cli/index.ts +16 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/capture-variable.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-assertion.hbs +7 -0
- package/src/generators/test-generator/patterns/capture-patterns.ts +59 -0
- package/src/generators/test-generator/patterns/index.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +1 -0
- package/src/generators/test-generator/utils/data-resolver.ts +20 -0
- package/src/harness/audit.ts +112 -0
- package/src/harness/blindspot.ts +51 -0
- package/src/harness/catalog/universal-viewpoints.yaml +114 -0
- package/src/harness/challenge.ts +131 -0
- package/src/harness/feedback.ts +84 -0
- package/src/harness/intent.ts +58 -0
- package/src/harness/ledger.ts +155 -0
- package/src/harness/manifest.ts +173 -0
- package/src/harness/parse.ts +145 -0
- package/src/harness/script-check.ts +149 -0
- package/src/harness/secret-scan.ts +51 -0
- package/src/harness/sensors.ts +279 -0
- package/src/harness/trace.ts +138 -0
- package/src/orchestrator/ai-rules-updater.ts +57 -10
- package/src/orchestrator/figma/spec-figma-renderer.ts +2 -2
- package/src/orchestrator/figma/spec-figma-section-renderers.ts +1 -1
- package/src/orchestrator/project-initializer.ts +10 -7
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +46 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-discovery.md +32 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-reviewer.md +37 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +3 -3
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-screen.md +5 -5
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +36 -12
- package/src/orchestrator/templates/ai-instructions/claude-cmd-design.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-feedback.md +36 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +27 -30
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +4 -1
- package/src/orchestrator/templates/ai-instructions/claude-config.md +1 -4
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-mcp.md +82 -0
- package/{dist/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md → src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-figma-pat.md} +14 -48
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-live.md +60 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-mode-local.md +38 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture.md +35 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-harness-audit.md +84 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +40 -1
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +3 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-screen.md +4 -4
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +18 -10
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-design.md +13 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-feedback.md +24 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +20 -30
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +2 -1
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +1 -4
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-mcp.md +82 -0
- package/{dist/orchestrator/templates/ai-instructions/claude-skill-figma-source.md → src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-figma-pat.md} +14 -48
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-live.md +60 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-mode-local.md +38 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture.md +35 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-harness-audit.md +84 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +1 -1
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +40 -1
- package/src/orchestrator/templates/specs-test-data.ts +9 -0
- package/src/tools/figma/figma-auth.ts +20 -9
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-figma.md +0 -142
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +0 -112
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/copilot-skill-figma-source.md +0 -151
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-figma.md +0 -142
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +0 -112
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-local.md +0 -73
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-figma-source.md +0 -151
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest + spec-fingerprint — make testcases reflect the SOFTWARE, not chase it.
|
|
3
|
+
*
|
|
4
|
+
* Records, per scenario, which spec section it was derived from + a hash of that
|
|
5
|
+
* section. On re-run, diff current spec hashes vs the manifest to classify each
|
|
6
|
+
* scenario: keep / regenerate / retire — so the orchestrator regenerates only the
|
|
7
|
+
* parts whose spec changed (stable + cheap), and never silently drifts.
|
|
8
|
+
*
|
|
9
|
+
* Deterministic. Manifest lives at .sungen/manifest/<screen>.json.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { createHash } from 'crypto';
|
|
14
|
+
|
|
15
|
+
export interface SpecSection { name: string; hash: string }
|
|
16
|
+
export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
|
|
17
|
+
export interface Manifest {
|
|
18
|
+
screen: string;
|
|
19
|
+
builtAt: string;
|
|
20
|
+
specSections: Record<string, string>; // section name → hash (snapshot at build time)
|
|
21
|
+
entries: ManifestEntry[]; // scenario ↔ section ↔ hash
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ChangeKind = 'keep' | 'regenerate' | 'retire';
|
|
25
|
+
export interface ChangePlan {
|
|
26
|
+
screen: string;
|
|
27
|
+
scenarios: { scenario: string; section: string; change: ChangeKind; reason: string }[];
|
|
28
|
+
newSections: string[]; // spec sections with no scenario yet → generate
|
|
29
|
+
removedSections: string[]; // sections in manifest no longer in spec
|
|
30
|
+
summary: { keep: number; regenerate: number; retire: number; newSections: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function norm(s: string): string {
|
|
34
|
+
return s.replace(/\(Tier[^)]*\)/i, '').replace(/[#*-]/g, '').trim().toLowerCase().replace(/\s+/g, ' ');
|
|
35
|
+
}
|
|
36
|
+
function shortHash(s: string): string {
|
|
37
|
+
return createHash('sha1').update(s).digest('hex').slice(0, 12);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const NO_SPEC = '(no-spec-link)';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Match a feature section name to a spec section name. Feature comments often
|
|
44
|
+
* combine spec sections (e.g. "features items / product cards" → spec "features
|
|
45
|
+
* items"), so fall back to longest substring match in either direction.
|
|
46
|
+
*/
|
|
47
|
+
function matchSection(featureSection: string, specNames: string[]): string | null {
|
|
48
|
+
if (specNames.includes(featureSection)) return featureSection;
|
|
49
|
+
let best: string | null = null;
|
|
50
|
+
for (const s of specNames) {
|
|
51
|
+
if (featureSection.includes(s) || s.includes(featureSection)) {
|
|
52
|
+
if (!best || s.length > best.length) best = s;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return best;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Parse spec.md into hashable sections (## and ### headings; "Section:" prefix stripped). */
|
|
59
|
+
export function parseSpecSections(specPath: string): SpecSection[] {
|
|
60
|
+
if (!fs.existsSync(specPath)) return [];
|
|
61
|
+
const lines = fs.readFileSync(specPath, 'utf-8').split('\n');
|
|
62
|
+
const sections: { name: string; body: string[] }[] = [];
|
|
63
|
+
let cur: { name: string; body: string[] } | null = null;
|
|
64
|
+
for (const line of lines) {
|
|
65
|
+
const h = line.match(/^#{2,3}\s+(.*)$/);
|
|
66
|
+
if (h) {
|
|
67
|
+
if (cur) sections.push(cur);
|
|
68
|
+
const name = h[1].replace(/^Section:\s*/i, '').trim();
|
|
69
|
+
cur = { name, body: [] };
|
|
70
|
+
} else if (cur) {
|
|
71
|
+
cur.body.push(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (cur) sections.push(cur);
|
|
75
|
+
return sections.map((s) => ({ name: norm(s.name), hash: shortHash(s.body.join('\n').trim()) }));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Parse feature into scenario → section mapping using `# --- Section: X ---` comments. */
|
|
79
|
+
function parseFeatureSections(featurePath: string): { scenario: string; vpCode?: string; section: string }[] {
|
|
80
|
+
if (!fs.existsSync(featurePath)) return [];
|
|
81
|
+
const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
|
|
82
|
+
const out: { scenario: string; vpCode?: string; section: string }[] = [];
|
|
83
|
+
let section = '(unsectioned)';
|
|
84
|
+
for (const raw of lines) {
|
|
85
|
+
const line = raw.trim();
|
|
86
|
+
const sc = line.match(/^#\s*-{2,}\s*Section:\s*(.*?)\s*-{2,}\s*$/i);
|
|
87
|
+
if (sc) { section = norm(sc[1]); continue; }
|
|
88
|
+
const s = line.match(/^Scenario:\s*(.*)$/);
|
|
89
|
+
if (s) {
|
|
90
|
+
const name = s[1].trim();
|
|
91
|
+
const code = name.match(/\bVP-[A-Z]+-\d+/i)?.[0];
|
|
92
|
+
out.push({ scenario: name, vpCode: code, section });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildManifest(screenDir: string, screenName: string): Manifest {
|
|
99
|
+
const specPath = path.join(screenDir, 'requirements', 'spec.md');
|
|
100
|
+
const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
|
|
101
|
+
const specSections = parseSpecSections(specPath);
|
|
102
|
+
const specMap: Record<string, string> = {};
|
|
103
|
+
for (const s of specSections) specMap[s.name] = s.hash;
|
|
104
|
+
|
|
105
|
+
const specNames = Object.keys(specMap);
|
|
106
|
+
const featScenarios = parseFeatureSections(featurePath);
|
|
107
|
+
const entries: ManifestEntry[] = featScenarios.map((f) => {
|
|
108
|
+
const matched = matchSection(f.section, specNames);
|
|
109
|
+
return {
|
|
110
|
+
scenario: f.scenario,
|
|
111
|
+
vpCode: f.vpCode,
|
|
112
|
+
section: matched ?? f.section, // canonical spec section when matched
|
|
113
|
+
specHash: matched ? specMap[matched] : NO_SPEC,
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
screen: screenName,
|
|
119
|
+
builtAt: new Date().toISOString(),
|
|
120
|
+
specSections: specMap,
|
|
121
|
+
entries,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function diffManifest(screenDir: string, screenName: string, manifest: Manifest): ChangePlan {
|
|
126
|
+
const specPath = path.join(screenDir, 'requirements', 'spec.md');
|
|
127
|
+
const current = parseSpecSections(specPath);
|
|
128
|
+
const currentMap: Record<string, string> = {};
|
|
129
|
+
for (const s of current) currentMap[s.name] = s.hash;
|
|
130
|
+
|
|
131
|
+
const scenarios = manifest.entries.map((e) => {
|
|
132
|
+
let change: ChangeKind; let reason: string;
|
|
133
|
+
if (e.specHash === NO_SPEC) { change = 'keep'; reason = 'not linked to a spec section (manual/cross-cutting)'; }
|
|
134
|
+
else {
|
|
135
|
+
const now = currentMap[e.section];
|
|
136
|
+
if (now === undefined) { change = 'retire'; reason = `spec section "${e.section}" no longer exists`; }
|
|
137
|
+
else if (now !== e.specHash) { change = 'regenerate'; reason = `spec section "${e.section}" changed (${e.specHash} → ${now})`; }
|
|
138
|
+
else { change = 'keep'; reason = 'spec section unchanged'; }
|
|
139
|
+
}
|
|
140
|
+
return { scenario: e.scenario, section: e.section, change, reason };
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const manifestSections = new Set(manifest.entries.map((e) => e.section));
|
|
144
|
+
const newSections = Object.keys(currentMap).filter((s) => !manifestSections.has(s) && !manifest.specSections[s]);
|
|
145
|
+
const removedSections = Object.keys(manifest.specSections).filter((s) => currentMap[s] === undefined);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
screen: screenName,
|
|
149
|
+
scenarios,
|
|
150
|
+
newSections,
|
|
151
|
+
removedSections,
|
|
152
|
+
summary: {
|
|
153
|
+
keep: scenarios.filter((s) => s.change === 'keep').length,
|
|
154
|
+
regenerate: scenarios.filter((s) => s.change === 'regenerate').length,
|
|
155
|
+
retire: scenarios.filter((s) => s.change === 'retire').length,
|
|
156
|
+
newSections: newSections.length,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function manifestPath(screenName: string): string {
|
|
162
|
+
return path.join(process.cwd(), '.sungen', 'manifest', `${screenName}.json`);
|
|
163
|
+
}
|
|
164
|
+
export function loadManifest(screenName: string): Manifest | null {
|
|
165
|
+
const p = manifestPath(screenName);
|
|
166
|
+
return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : null;
|
|
167
|
+
}
|
|
168
|
+
export function saveManifest(m: Manifest): string {
|
|
169
|
+
const p = manifestPath(m.screen);
|
|
170
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
171
|
+
fs.writeFileSync(p, JSON.stringify(m, null, 2), 'utf-8');
|
|
172
|
+
return p;
|
|
173
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness parsers — load the two test-design artifacts the sensors operate on:
|
|
3
|
+
* 1. requirements/test-viewpoint.md → viewpoint-overview (id, priority, group)
|
|
4
|
+
* 2. features/<screen>.feature → scenarios (vp code, priority, Then-shape)
|
|
5
|
+
*
|
|
6
|
+
* Reuses the existing GherkinParser (framework-agnostic) for the .feature file.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
|
+
|
|
11
|
+
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
12
|
+
|
|
13
|
+
export interface ViewpointEntry {
|
|
14
|
+
id: string; // e.g. VP-DATA-CONSISTENCY
|
|
15
|
+
priority: 'High' | 'Medium' | 'Low' | 'Unknown';
|
|
16
|
+
reason: string;
|
|
17
|
+
group?: 'Required' | 'Recommended' | 'Optional';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ScenarioInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
vpCode?: string; // VP-CART-001 style code in the scenario title
|
|
23
|
+
category?: string; // CART, LIST, VAL, ... (from the code)
|
|
24
|
+
priority: Priority;
|
|
25
|
+
manual: boolean;
|
|
26
|
+
thenCount: number;
|
|
27
|
+
hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
|
|
28
|
+
shallow: boolean; // asserts only visibility/navigation (no data)
|
|
29
|
+
stepSkeleton: string; // normalized steps for duplicate clustering
|
|
30
|
+
haystack: string; // lowercase name + steps text (for keyword coverage)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------- test-viewpoint.md ----------
|
|
34
|
+
|
|
35
|
+
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
36
|
+
if (!fs.existsSync(filePath)) return [];
|
|
37
|
+
const text = fs.readFileSync(filePath, 'utf-8');
|
|
38
|
+
const lines = text.split('\n');
|
|
39
|
+
|
|
40
|
+
const entries = new Map<string, ViewpointEntry>();
|
|
41
|
+
|
|
42
|
+
// 1) Priority Viewpoints table: | VP | Priority | Reason |
|
|
43
|
+
let inPriorityTable = false;
|
|
44
|
+
for (const raw of lines) {
|
|
45
|
+
const line = raw.trim();
|
|
46
|
+
if (/^##\s+Priority Viewpoints/i.test(line)) { inPriorityTable = true; continue; }
|
|
47
|
+
if (inPriorityTable && /^##\s/.test(line)) { inPriorityTable = false; }
|
|
48
|
+
if (inPriorityTable && line.startsWith('|')) {
|
|
49
|
+
const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
50
|
+
if (cells.length >= 3) {
|
|
51
|
+
const id = cells[0];
|
|
52
|
+
if (/^VP[-A-Z0-9]/i.test(id) && !/^vp$/i.test(id) && !/^-+$/.test(cells[1])) {
|
|
53
|
+
const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
|
|
54
|
+
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 2) Viewpoint Grouping: ### Required / ### Recommended / ### Optional → bullet list
|
|
61
|
+
let group: ViewpointEntry['group'] | undefined;
|
|
62
|
+
for (const raw of lines) {
|
|
63
|
+
const line = raw.trim();
|
|
64
|
+
const g = line.match(/^###\s+(Required|Recommended|Optional)/i);
|
|
65
|
+
if (g) { group = (g[1][0].toUpperCase() + g[1].slice(1).toLowerCase()) as any; continue; }
|
|
66
|
+
if (/^##\s/.test(line)) { group = undefined; }
|
|
67
|
+
if (group) {
|
|
68
|
+
const m = line.match(/^-\s+(VP[-A-Z0-9]+)/i);
|
|
69
|
+
if (m) {
|
|
70
|
+
const id = m[1].toUpperCase();
|
|
71
|
+
const existing = entries.get(id);
|
|
72
|
+
if (existing) existing.group = group;
|
|
73
|
+
else entries.set(id, { id, priority: 'Unknown', reason: '', group });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [...entries.values()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------- .feature ----------
|
|
82
|
+
|
|
83
|
+
const PRIORITY_TAGS: Record<string, Priority> = { '@high': 'high', '@normal': 'normal', '@low': 'low' };
|
|
84
|
+
|
|
85
|
+
function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
86
|
+
const tags = sc.tags || [];
|
|
87
|
+
const manual = tags.includes('@manual');
|
|
88
|
+
let priority: Priority = 'unknown';
|
|
89
|
+
for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
|
|
90
|
+
|
|
91
|
+
const codeMatch = sc.name.match(/\bVP-([A-Z]+)-\d+/i);
|
|
92
|
+
const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
|
|
93
|
+
const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
|
|
94
|
+
|
|
95
|
+
// Then-phase detection (And/But inherit previous primary keyword)
|
|
96
|
+
let last = 'Given';
|
|
97
|
+
let thenCount = 0;
|
|
98
|
+
let hasData = false;
|
|
99
|
+
const skeletonParts: string[] = [];
|
|
100
|
+
const textParts: string[] = [sc.name];
|
|
101
|
+
|
|
102
|
+
for (const step of sc.steps as ParsedStep[]) {
|
|
103
|
+
const kw = step.keyword.trim();
|
|
104
|
+
if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
|
|
105
|
+
textParts.push(step.text);
|
|
106
|
+
// normalized skeleton: keep [refs] (distinct targets = distinct tests),
|
|
107
|
+
// but neutralize {{vars}} and quoted values so EP/data families collapse.
|
|
108
|
+
const skel = step.text
|
|
109
|
+
.replace(/\{\{[^}]*\}\}/g, '{}')
|
|
110
|
+
.replace(/"[^"]*"/g, '""')
|
|
111
|
+
.replace(/\s+/g, ' ')
|
|
112
|
+
.trim()
|
|
113
|
+
.toLowerCase();
|
|
114
|
+
skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
|
|
115
|
+
|
|
116
|
+
if (last === 'Then') {
|
|
117
|
+
thenCount++;
|
|
118
|
+
if (/\{\{|contains|match data|toHaveText/i.test(step.text)) hasData = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const shallow = thenCount > 0 && !hasData;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
name: sc.name,
|
|
126
|
+
vpCode,
|
|
127
|
+
category,
|
|
128
|
+
priority,
|
|
129
|
+
manual,
|
|
130
|
+
thenCount,
|
|
131
|
+
hasDataAssertion: hasData,
|
|
132
|
+
shallow,
|
|
133
|
+
stepSkeleton: skeletonParts.join(' | '),
|
|
134
|
+
haystack: textParts.join(' ').toLowerCase(),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function loadScenarios(featurePath: string): ScenarioInfo[] {
|
|
139
|
+
if (!fs.existsSync(featurePath)) return [];
|
|
140
|
+
const parser = new GherkinParser();
|
|
141
|
+
const feature = parser.parseFeatureFile(featurePath);
|
|
142
|
+
return (feature.scenarios || [])
|
|
143
|
+
.filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
|
|
144
|
+
.map(classifyScenario);
|
|
145
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script-check — verify the generated Playwright spec is a faithful 1:1 of the
|
|
3
|
+
* Gherkin feature, i.e. "the testcase and the test code are not two different things".
|
|
4
|
+
*
|
|
5
|
+
* Two deterministic checks:
|
|
6
|
+
* A. Structural 1:1 — every non-@manual / non-@steps scenario has exactly one
|
|
7
|
+
* matching `test('<title>')` block in the committed spec (and no extras).
|
|
8
|
+
* B. Drift — regenerate the spec from the SAME .feature + selectors + test-data
|
|
9
|
+
* into a temp dir and diff against the committed spec. Any difference means
|
|
10
|
+
* the committed spec was hand-edited or is stale (feature changed without a
|
|
11
|
+
* regenerate) → the script no longer reflects the testcase.
|
|
12
|
+
*
|
|
13
|
+
* Pure-deterministic (reuses the compiler). No AI.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import { loadScenarios } from './parse';
|
|
19
|
+
|
|
20
|
+
export interface ScriptCheckResult {
|
|
21
|
+
screen: string;
|
|
22
|
+
specPath: string | null;
|
|
23
|
+
automatedScenarios: number; // non-manual, non-steps
|
|
24
|
+
manualScenarios: number;
|
|
25
|
+
specTestBlocks: number;
|
|
26
|
+
countMatch: boolean;
|
|
27
|
+
missingInSpec: string[]; // scenario titles with no test() block
|
|
28
|
+
extraInSpec: string[]; // test() titles with no scenario
|
|
29
|
+
drift: 'in-sync' | 'drift' | 'no-spec';
|
|
30
|
+
driftHunks: string[]; // sample differing lines (committed vs regenerated)
|
|
31
|
+
status: 'OK' | 'FAIL';
|
|
32
|
+
findings: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractTestTitles(specSrc: string): string[] {
|
|
36
|
+
// Count real test cases only: test(...), test.only/.skip/.fixme(...).
|
|
37
|
+
// Exclude test.describe / test.beforeAll / hooks (not test cases).
|
|
38
|
+
const titles: string[] = [];
|
|
39
|
+
const re = /\btest(?:\.(?:only|skip|fixme))?\(\s*(['"`])([^'"`]+)\1/g;
|
|
40
|
+
let m: RegExpExecArray | null;
|
|
41
|
+
while ((m = re.exec(specSrc))) titles.push(m[2].trim());
|
|
42
|
+
return titles;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalize(src: string): string {
|
|
46
|
+
return src
|
|
47
|
+
.split('\n')
|
|
48
|
+
.map((l) => l.replace(/\s+$/, ''))
|
|
49
|
+
.join('\n')
|
|
50
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
51
|
+
.trim();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findSpec(dir: string, screen: string): string | null {
|
|
55
|
+
// generated spec: <dir>/<screen>/<feature>.spec.ts (or nested)
|
|
56
|
+
const hits: string[] = [];
|
|
57
|
+
const walk = (d: string) => {
|
|
58
|
+
if (!fs.existsSync(d)) return;
|
|
59
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true })) {
|
|
60
|
+
const p = path.join(d, e.name);
|
|
61
|
+
if (e.isDirectory()) walk(p);
|
|
62
|
+
else if (e.name.endsWith('.spec.ts')) hits.push(p);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
walk(dir);
|
|
66
|
+
return hits[0] ?? null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function runScriptCheck(screenDir: string, screenName: string, flowMode: boolean): Promise<ScriptCheckResult> {
|
|
70
|
+
const featurePath = path.join(screenDir, 'features', `${screenName}.feature`);
|
|
71
|
+
const scenarios = loadScenarios(featurePath);
|
|
72
|
+
const automated = scenarios.filter((s) => !s.manual);
|
|
73
|
+
const manual = scenarios.filter((s) => s.manual);
|
|
74
|
+
|
|
75
|
+
const committedSpec = findSpec(path.join(process.cwd(), 'specs', 'generated'), screenName);
|
|
76
|
+
|
|
77
|
+
const findings: string[] = [];
|
|
78
|
+
let specTitles: string[] = [];
|
|
79
|
+
let specSrc = '';
|
|
80
|
+
if (committedSpec) {
|
|
81
|
+
specSrc = fs.readFileSync(committedSpec, 'utf-8');
|
|
82
|
+
specTitles = extractTestTitles(specSrc);
|
|
83
|
+
} else {
|
|
84
|
+
findings.push('No generated spec found under specs/generated/ — run `sungen generate` / `/sungen:run-test` first.');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A. Structural 1:1
|
|
88
|
+
const specTitleSet = new Set(specTitles);
|
|
89
|
+
const scenTitleSet = new Set(automated.map((s) => s.name));
|
|
90
|
+
const missingInSpec = automated.filter((s) => !specTitleSet.has(s.name)).map((s) => s.name);
|
|
91
|
+
const extraInSpec = specTitles.filter((t) => !scenTitleSet.has(t));
|
|
92
|
+
const countMatch = committedSpec ? automated.length === specTitles.length : false;
|
|
93
|
+
if (committedSpec && !countMatch) {
|
|
94
|
+
findings.push(`Count mismatch: ${automated.length} automated scenarios vs ${specTitles.length} test() blocks.`);
|
|
95
|
+
}
|
|
96
|
+
for (const t of missingInSpec) findings.push(`MISSING in spec: scenario "${t}" has no test() block (stale spec — regenerate).`);
|
|
97
|
+
for (const t of extraInSpec) findings.push(`EXTRA in spec: test "${t}" has no matching scenario (hand-edited spec).`);
|
|
98
|
+
|
|
99
|
+
// B. Drift — regenerate to temp and diff
|
|
100
|
+
let drift: ScriptCheckResult['drift'] = committedSpec ? 'in-sync' : 'no-spec';
|
|
101
|
+
const driftHunks: string[] = [];
|
|
102
|
+
if (committedSpec) {
|
|
103
|
+
try {
|
|
104
|
+
const { CodeGenerator } = require('../generators/test-generator/code-generator');
|
|
105
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sungen-scriptcheck-'));
|
|
106
|
+
const qaSourceDir = path.join(process.cwd(), 'qa', flowMode ? 'flows' : 'screens');
|
|
107
|
+
const gen = new CodeGenerator({ framework: 'playwright', screenName, runtimeData: true, flowMode });
|
|
108
|
+
await gen.generateAllTests(qaSourceDir, tmp, [featurePath]);
|
|
109
|
+
const fresh = findSpec(tmp, screenName);
|
|
110
|
+
if (fresh) {
|
|
111
|
+
const a = normalize(specSrc);
|
|
112
|
+
const b = normalize(fs.readFileSync(fresh, 'utf-8'));
|
|
113
|
+
if (a !== b) {
|
|
114
|
+
drift = 'drift';
|
|
115
|
+
// collect a few differing lines
|
|
116
|
+
const al = a.split('\n'), bl = b.split('\n');
|
|
117
|
+
const max = Math.max(al.length, bl.length);
|
|
118
|
+
for (let i = 0, shown = 0; i < max && shown < 6; i++) {
|
|
119
|
+
if (al[i] !== bl[i]) {
|
|
120
|
+
driftHunks.push(` L${i + 1}\n committed: ${(al[i] ?? '∅').trim().slice(0, 100)}\n expected : ${(bl[i] ?? '∅').trim().slice(0, 100)}`);
|
|
121
|
+
shown++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
findings.push('DRIFT: committed spec differs from a fresh regenerate → spec was hand-edited or the .feature changed without `sungen generate`. The test code no longer reflects the Gherkin.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
128
|
+
} catch (e) {
|
|
129
|
+
findings.push(`Drift check skipped (regenerate failed): ${e instanceof Error ? e.message : e}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ok = !!committedSpec && countMatch && missingInSpec.length === 0 && extraInSpec.length === 0 && drift === 'in-sync';
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
screen: screenName,
|
|
137
|
+
specPath: committedSpec,
|
|
138
|
+
automatedScenarios: automated.length,
|
|
139
|
+
manualScenarios: manual.length,
|
|
140
|
+
specTestBlocks: specTitles.length,
|
|
141
|
+
countMatch,
|
|
142
|
+
missingInSpec,
|
|
143
|
+
extraInSpec,
|
|
144
|
+
drift,
|
|
145
|
+
driftHunks,
|
|
146
|
+
status: ok ? 'OK' : 'FAIL',
|
|
147
|
+
findings,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secret scanner (security S0) — warn (never block) when test-data appears to
|
|
3
|
+
* contain a REAL secret rather than a safe placeholder/test value.
|
|
4
|
+
*
|
|
5
|
+
* test-data/*.yaml is committed by design, so a real API key / prod token there is
|
|
6
|
+
* a leak waiting to happen. We flag high-confidence signals only, to avoid nagging
|
|
7
|
+
* about ordinary test passwords ("Test@123") or `{{vars}}`.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
export interface SecretHit { file: string; line: number; reason: string }
|
|
13
|
+
|
|
14
|
+
// High-confidence vendor token prefixes (very low false-positive rate).
|
|
15
|
+
const VENDOR_PREFIXES = /\b(figd_[A-Za-z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|gho_[A-Za-z0-9]{20,}|glpat-[A-Za-z0-9_-]{16,}|sk-[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{12,}|xox[baprs]-[A-Za-z0-9-]{10,}|AIza[0-9A-Za-z_-]{30,})/;
|
|
16
|
+
// A secret-named key assigned a long, non-placeholder value.
|
|
17
|
+
const SECRET_KEY = /\b(password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret)\b\s*:\s*(.+)$/i;
|
|
18
|
+
const PLACEHOLDERish = /\{\{|<.*>|changeme|example|dummy|test\b|placeholder|xxxx|\*\*\*/i;
|
|
19
|
+
|
|
20
|
+
function scanText(text: string, file: string): SecretHit[] {
|
|
21
|
+
const hits: SecretHit[] = [];
|
|
22
|
+
text.split('\n').forEach((raw, i) => {
|
|
23
|
+
const line = i + 1;
|
|
24
|
+
if (VENDOR_PREFIXES.test(raw)) {
|
|
25
|
+
hits.push({ file, line, reason: 'looks like a real vendor API token/key' });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const m = raw.match(SECRET_KEY);
|
|
29
|
+
if (m) {
|
|
30
|
+
const val = m[2].trim().replace(/^["']|["']$/g, '');
|
|
31
|
+
// Long, high-entropy-ish, not an obvious placeholder/test value.
|
|
32
|
+
if (val.length >= 20 && !PLACEHOLDERish.test(raw) && /[A-Za-z]/.test(val) && /[0-9]/.test(val)) {
|
|
33
|
+
hits.push({ file, line, reason: `secret-named key "${m[1]}" with a long literal value` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return hits;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Scan a screen/flow dir's test-data/*.yaml for likely real secrets. */
|
|
41
|
+
export function scanTestDataSecrets(baseDir: string): SecretHit[] {
|
|
42
|
+
const tdDir = path.join(baseDir, 'test-data');
|
|
43
|
+
if (!fs.existsSync(tdDir)) return [];
|
|
44
|
+
const hits: SecretHit[] = [];
|
|
45
|
+
for (const f of fs.readdirSync(tdDir)) {
|
|
46
|
+
if (!/\.ya?ml$/i.test(f)) continue;
|
|
47
|
+
const p = path.join(tdDir, f);
|
|
48
|
+
try { hits.push(...scanText(fs.readFileSync(p, 'utf-8'), path.join('test-data', f))); } catch { /* ignore */ }
|
|
49
|
+
}
|
|
50
|
+
return hits;
|
|
51
|
+
}
|