@sun-asterisk/sungen 3.0.0 → 3.0.1
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/harness/audit.d.ts +16 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +68 -4
- 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 +13 -0
- package/dist/harness/capability-plan.js.map +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/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/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-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-ingest-legacy.md +79 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
- package/package.json +3 -3
- 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/harness/audit.ts +81 -4
- package/src/harness/capability-plan.ts +11 -0
- package/src/harness/eval/skill-lint.ts +87 -0
- package/src/harness/parse.ts +19 -3
- package/src/harness/quality-gates.ts +152 -0
- 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-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-ingest-legacy.md +79 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +44 -7
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sungen-ingest-legacy
|
|
3
|
+
description: 'Import a legacy manual testcase suite from Google Sheets (multi-tab) or a local file into Sungen — fetch via MCP, then sungen ingest. Use when the user wants to convert/evaluate an existing manual testcase spreadsheet.'
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# sungen-ingest-legacy
|
|
8
|
+
|
|
9
|
+
Bring an existing **manual testcase workbook** into Sungen for evaluation + conversion. The
|
|
10
|
+
fetch (Google login + pick file) is done here via MCP; the parsing/audit is deterministic
|
|
11
|
+
(`sungen ingest`). **Security:** the workbook is the user's project data — read it on
|
|
12
|
+
consent, keep the output in their project, never upload or commit the content.
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
1. **Locate the source.**
|
|
17
|
+
- **Google Sheets (recommended):** use the Google Drive MCP. Authenticate if needed, then
|
|
18
|
+
`search_files` / list to find the workbook; confirm the file with the user.
|
|
19
|
+
- **Local:** if the user points to a `.xlsx`/`.csv`, skip to step 4.
|
|
20
|
+
|
|
21
|
+
2. **List the tabs.** Read the workbook's sheet/tab names (Drive MCP file metadata or a values
|
|
22
|
+
read). A legacy workbook usually has **many** tabs — some are testcases, some are
|
|
23
|
+
viewpoint/UI matrices.
|
|
24
|
+
|
|
25
|
+
3. **Assemble a JSON sheet-bundle.** For each tab, read its cell values (a 2-D array of
|
|
26
|
+
strings) and write a local bundle in the user project (e.g.
|
|
27
|
+
`qa/screens/<screen>/requirements/legacy/bundle.json`):
|
|
28
|
+
```json
|
|
29
|
+
{ "source": "<workbook name>", "sheets": [ { "name": "<tab>", "rows": [["TC ID","Page",…],["TC-01",…]] } ] }
|
|
30
|
+
```
|
|
31
|
+
Record only the **source link** in `requirements/legacy/source.yaml` — never the content.
|
|
32
|
+
|
|
33
|
+
4. **Classify the tabs.** Run:
|
|
34
|
+
```bash
|
|
35
|
+
sungen ingest --legacy <bundle.json|file.xlsx|file.csv> --list-sheets
|
|
36
|
+
```
|
|
37
|
+
It prints each tab + detected type (`testcase` / `viewpoint-matrix` / `ui-checklist`).
|
|
38
|
+
|
|
39
|
+
5. **Confirm which tabs to ingest.** Use `AskUserQuestion` to let the user pick the
|
|
40
|
+
**testcase** tabs (matrix/UI tabs feed the viewpoint layer later, not the inventory).
|
|
41
|
+
|
|
42
|
+
6. **Ingest + reconcile.**
|
|
43
|
+
```bash
|
|
44
|
+
sungen ingest --legacy <source> --screen <screen> --sheets "<Tab A>,<Tab B>" --emit-gherkin
|
|
45
|
+
```
|
|
46
|
+
Produces: `inventory.json` (+ baseline audit), `*.legacy-draft.feature` + `legacy-trace.json`
|
|
47
|
+
(parity: `@legacy:<id>` per scenario), and `test-viewpoint.draft.md` with **blind-spots**
|
|
48
|
+
(catalog-expected viewpoints the legacy suite lacks).
|
|
49
|
+
|
|
50
|
+
7. **Hand off to quality.** Tell the user the next step is `/sungen:create-test <screen>` —
|
|
51
|
+
it discovers + refines the draft into real `[Reference]` steps and fills the blind-spots;
|
|
52
|
+
then `sungen audit <screen>` gates quality. A 1:1 convert is NOT the deliverable; the
|
|
53
|
+
harness raises the legacy floor to catalog quality.
|
|
54
|
+
|
|
55
|
+
## Governance block (important)
|
|
56
|
+
|
|
57
|
+
Many orgs mark confidential files as **"ineligible for generative AI contexts"** — the
|
|
58
|
+
Google Drive MCP will then **refuse** to read the file (metadata + download both error).
|
|
59
|
+
This is the org's DLP policy, not a bug, and it is the *expected* outcome for a
|
|
60
|
+
confidential testcase suite. When you hit it, **do not retry** — fall back:
|
|
61
|
+
|
|
62
|
+
> "This sheet is restricted by your org's data policy, so I can't read it through the
|
|
63
|
+
> AI connector. Two ways to proceed, both running as **you**, not AI:
|
|
64
|
+
> (1) `sungen ingest --gsheet <url>` — fetches under your own Google identity
|
|
65
|
+
> (read-only; Viewer/Commenter is enough). It offers to install `googleapis`
|
|
66
|
+
> and to open the Google login in your browser (pick your account), then
|
|
67
|
+
> retries automatically. Needs the gcloud SDK for the browser login.
|
|
68
|
+
> (2) Export it manually (**File → Download → Microsoft Excel `.xlsx`**) and I'll run
|
|
69
|
+
> `sungen ingest --legacy <file>.xlsx`."
|
|
70
|
+
|
|
71
|
+
The local-file path is deterministic and **never sends the content through AI** — the
|
|
72
|
+
correct, governance-compliant channel for confidential data. The MCP auto-pick is only
|
|
73
|
+
for files the org does *not* restrict.
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
- Multiple local CSVs (one per tab) also work: `--legacy tab1.csv tab2.csv …`.
|
|
77
|
+
- Re-run only re-fetches when the user asks; otherwise reuse the saved bundle.
|
|
78
|
+
- Do not invent testcases. Only ingest what the workbook contains; the *augmentation*
|
|
79
|
+
(blind-spots) happens in `/sungen:create-test`, flagged for human review.
|
|
@@ -105,6 +105,9 @@ Auto-detected by `create-test` before invoking this skill:
|
|
|
105
105
|
2. Each row / bullet / item = 1 viewpoint → add to `Viewpoint items` in Coverage Map.
|
|
106
106
|
3. Do NOT pre-classify into buckets before scanning — classify only when
|
|
107
107
|
writing the scenario.
|
|
108
|
+
4. **If it declares viewpoint IDs** (e.g. `VP0`, `VP1`…`VP12`, `MS-HP-001`), capture each
|
|
109
|
+
item WITH its ID and **reuse that ID as the scenario code** — do not invent a generic
|
|
110
|
+
`VP-<CAT>` scheme (the harness Taxonomy-match gate FAILs on mismatch).
|
|
108
111
|
- `qa/context.md` — project-wide context set by the QA lead. Read ONCE before building the Coverage Map; apply to every screen. Extraction rules:
|
|
109
112
|
- **Roles** → for each role in the table: add to the `@auth:X` tag pool; generate a VP-SEC blocked-access scenario for every role boundary relevant to this screen.
|
|
110
113
|
- **Testing strategy → Focus areas** → if `security` listed: VP-SEC is mandatory Tier 1 for every free-text input regardless of spec risk level; if `ui` not listed: all VP-UI scenarios move to Tier 2 minimum.
|
|
@@ -260,6 +263,27 @@ Security: [S1 – admin only]
|
|
|
260
263
|
|
|
261
264
|
**Balance:** cover all the above (deep) BEFORE expanding subscription / UI-presence / extra validation edge cases. Do not over-invest in subscription while cart/detail/filter correctness are shallow.
|
|
262
265
|
|
|
266
|
+
#### Harness gates — satisfy on the FIRST pass (don't make the repair loop fix them)
|
|
267
|
+
|
|
268
|
+
`sungen audit` enforces these. Generate compliant output up front:
|
|
269
|
+
|
|
270
|
+
1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
|
|
271
|
+
2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
|
|
272
|
+
3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
|
|
273
|
+
- **Negative / absence claims** (`does not` / `no` / `never` / `prevents` / `không` / `chưa` — any language; `no-side-effect/no-duplicate`, `negative-claim/absence`): the `Then` must **differ** between the claim holding and not holding. A terminal `see [X] page` that looks identical whether or not the bad thing happened proves nothing. For a side-effect that should NOT repeat (re-submit on back, re-charge, duplicate order, resend OTP), assert the **count is unchanged** (`User see [Records] table with {{one}}` / `row with {{count}}`); if it's not UI-observable, mark `@manual` with a request-count oracle (shape below). This is general — it covers any side-effect, not a fixed verb list.
|
|
274
|
+
4. **Downstream-scope** (`DOWNSTREAM-SCOPE-MISSING`) — when the spec's Navigation Flow / success target is **another screen** (e.g. a confirmation/sent page), don't stop at a terminal `see [X] page`. Either cover that screen's content/guards (if its viewpoint items are in scope — they often have their own `MS-*` IDs), or scaffold it (`sungen add --screen <name>`) and note the handoff. Do not silently drop the downstream surface.
|
|
275
|
+
5. **Manual-oracle** (`MANUAL-STEPS-INSUFFICIENT`) — every `@manual` scenario needs **setup · action · observable expected · oracle/tool**, not a one-line note. Use this comment shape:
|
|
276
|
+
```gherkin
|
|
277
|
+
@high @manual
|
|
278
|
+
Scenario: VP-… <claim>
|
|
279
|
+
# MANUAL: <why it can't be automated — needs network capture / inbox / screen-reader / multi-tab>
|
|
280
|
+
# Tester verifies:
|
|
281
|
+
# 1. <setup> e.g. seed a registered email; throttle the network
|
|
282
|
+
# 2. <action> e.g. click [Submit] with the request in flight
|
|
283
|
+
# 3. <observable> e.g. only ONE POST is dispatched
|
|
284
|
+
# 4. Oracle: <tool> e.g. DevTools Network panel / mail-catcher / NVDA
|
|
285
|
+
```
|
|
286
|
+
|
|
263
287
|
#### Tier 1 guard — minimum before writing scenarios
|
|
264
288
|
|
|
265
289
|
| Spec section | Minimum requirement | Tag |
|
|
@@ -376,7 +400,7 @@ Add cleanup tags per the `sungen-gherkin-syntax` Cleanup table. Key rules:
|
|
|
376
400
|
**Files:** `qa/screens/<screen>/features/<screen>.feature` + `qa/screens/<screen>/test-data/<screen>.yaml`
|
|
377
401
|
|
|
378
402
|
Use step patterns and element types from `sungen-gherkin-syntax`.
|
|
379
|
-
**Naming**: `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
|
|
403
|
+
**Naming**: reuse the **project's `test-viewpoint.md` IDs** when it declares them (e.g. `VP0`, `MS-HP-001`); otherwise `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
|
|
380
404
|
|
|
381
405
|
**Test data** — grouped by section, loaded at runtime:
|
|
382
406
|
|
|
@@ -78,6 +78,16 @@ The CLI reads the **per-target result file first** (co-located with `.spec.ts`),
|
|
|
78
78
|
|
|
79
79
|
---
|
|
80
80
|
|
|
81
|
+
## XLSX sheets — Auto / Manual split
|
|
82
|
+
|
|
83
|
+
The `.xlsx` is split into two sheets so QA manages the sets separately:
|
|
84
|
+
- **`Auto`** — automatable test cases (`Auto` + `Not compiled`).
|
|
85
|
+
- **`Manual`** — `@manual` test cases (always present, header-only when there are none).
|
|
86
|
+
|
|
87
|
+
Multi-locale (no `SUNGEN_ENV`): one **`<LOCALE> Auto`** sheet per locale + a single shared **`Manual`** sheet (manual TCs are locale-invariant). The **CSV stays one file with every row** — the `Testcase type` column distinguishes Auto vs Manual.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
81
91
|
## Excluded from CSV
|
|
82
92
|
|
|
83
93
|
- `@steps:<name>` **base** scenarios — these are setup-only, inlined into `@extend:...` scenarios at compile time
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sungen-ingest-legacy
|
|
3
|
+
description: 'Import a legacy manual testcase suite from Google Sheets (multi-tab) or a local file into Sungen — fetch via MCP, then sungen ingest. Use when the user wants to convert/evaluate an existing manual testcase spreadsheet.'
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# sungen-ingest-legacy
|
|
8
|
+
|
|
9
|
+
Bring an existing **manual testcase workbook** into Sungen for evaluation + conversion. The
|
|
10
|
+
fetch (Google login + pick file) is done here via MCP; the parsing/audit is deterministic
|
|
11
|
+
(`sungen ingest`). **Security:** the workbook is the user's project data — read it on
|
|
12
|
+
consent, keep the output in their project, never upload or commit the content.
|
|
13
|
+
|
|
14
|
+
## Flow
|
|
15
|
+
|
|
16
|
+
1. **Locate the source.**
|
|
17
|
+
- **Google Sheets (recommended):** use the Google Drive MCP. Authenticate if needed, then
|
|
18
|
+
`search_files` / list to find the workbook; confirm the file with the user.
|
|
19
|
+
- **Local:** if the user points to a `.xlsx`/`.csv`, skip to step 4.
|
|
20
|
+
|
|
21
|
+
2. **List the tabs.** Read the workbook's sheet/tab names (Drive MCP file metadata or a values
|
|
22
|
+
read). A legacy workbook usually has **many** tabs — some are testcases, some are
|
|
23
|
+
viewpoint/UI matrices.
|
|
24
|
+
|
|
25
|
+
3. **Assemble a JSON sheet-bundle.** For each tab, read its cell values (a 2-D array of
|
|
26
|
+
strings) and write a local bundle in the user project (e.g.
|
|
27
|
+
`qa/screens/<screen>/requirements/legacy/bundle.json`):
|
|
28
|
+
```json
|
|
29
|
+
{ "source": "<workbook name>", "sheets": [ { "name": "<tab>", "rows": [["TC ID","Page",…],["TC-01",…]] } ] }
|
|
30
|
+
```
|
|
31
|
+
Record only the **source link** in `requirements/legacy/source.yaml` — never the content.
|
|
32
|
+
|
|
33
|
+
4. **Classify the tabs.** Run:
|
|
34
|
+
```bash
|
|
35
|
+
sungen ingest --legacy <bundle.json|file.xlsx|file.csv> --list-sheets
|
|
36
|
+
```
|
|
37
|
+
It prints each tab + detected type (`testcase` / `viewpoint-matrix` / `ui-checklist`).
|
|
38
|
+
|
|
39
|
+
5. **Confirm which tabs to ingest.** Use `AskUserQuestion` to let the user pick the
|
|
40
|
+
**testcase** tabs (matrix/UI tabs feed the viewpoint layer later, not the inventory).
|
|
41
|
+
|
|
42
|
+
6. **Ingest + reconcile.**
|
|
43
|
+
```bash
|
|
44
|
+
sungen ingest --legacy <source> --screen <screen> --sheets "<Tab A>,<Tab B>" --emit-gherkin
|
|
45
|
+
```
|
|
46
|
+
Produces: `inventory.json` (+ baseline audit), `*.legacy-draft.feature` + `legacy-trace.json`
|
|
47
|
+
(parity: `@legacy:<id>` per scenario), and `test-viewpoint.draft.md` with **blind-spots**
|
|
48
|
+
(catalog-expected viewpoints the legacy suite lacks).
|
|
49
|
+
|
|
50
|
+
7. **Hand off to quality.** Tell the user the next step is `/sungen:create-test <screen>` —
|
|
51
|
+
it discovers + refines the draft into real `[Reference]` steps and fills the blind-spots;
|
|
52
|
+
then `sungen audit <screen>` gates quality. A 1:1 convert is NOT the deliverable; the
|
|
53
|
+
harness raises the legacy floor to catalog quality.
|
|
54
|
+
|
|
55
|
+
## Governance block (important)
|
|
56
|
+
|
|
57
|
+
Many orgs mark confidential files as **"ineligible for generative AI contexts"** — the
|
|
58
|
+
Google Drive MCP will then **refuse** to read the file (metadata + download both error).
|
|
59
|
+
This is the org's DLP policy, not a bug, and it is the *expected* outcome for a
|
|
60
|
+
confidential testcase suite. When you hit it, **do not retry** — fall back:
|
|
61
|
+
|
|
62
|
+
> "This sheet is restricted by your org's data policy, so I can't read it through the
|
|
63
|
+
> AI connector. Two ways to proceed, both running as **you**, not AI:
|
|
64
|
+
> (1) `sungen ingest --gsheet <url>` — fetches under your own Google identity
|
|
65
|
+
> (read-only; Viewer/Commenter is enough). It offers to install `googleapis`
|
|
66
|
+
> and to open the Google login in your browser (pick your account), then
|
|
67
|
+
> retries automatically. Needs the gcloud SDK for the browser login.
|
|
68
|
+
> (2) Export it manually (**File → Download → Microsoft Excel `.xlsx`**) and I'll run
|
|
69
|
+
> `sungen ingest --legacy <file>.xlsx`."
|
|
70
|
+
|
|
71
|
+
The local-file path is deterministic and **never sends the content through AI** — the
|
|
72
|
+
correct, governance-compliant channel for confidential data. The MCP auto-pick is only
|
|
73
|
+
for files the org does *not* restrict.
|
|
74
|
+
|
|
75
|
+
## Notes
|
|
76
|
+
- Multiple local CSVs (one per tab) also work: `--legacy tab1.csv tab2.csv …`.
|
|
77
|
+
- Re-run only re-fetches when the user asks; otherwise reuse the saved bundle.
|
|
78
|
+
- Do not invent testcases. Only ingest what the workbook contains; the *augmentation*
|
|
79
|
+
(blind-spots) happens in `/sungen:create-test`, flagged for human review.
|
|
@@ -105,6 +105,17 @@ Auto-detected by `create-test` before invoking this skill:
|
|
|
105
105
|
2. Each row / bullet / item = 1 viewpoint → add to `Viewpoint items` in Coverage Map.
|
|
106
106
|
3. Do NOT pre-classify into buckets before scanning — classify only when
|
|
107
107
|
writing the scenario.
|
|
108
|
+
4. **If it declares viewpoint IDs** (e.g. `VP0`, `VP1`…`VP12`, `MS-HP-001`), capture each
|
|
109
|
+
item WITH its ID and **reuse that ID as the scenario code** — do not invent a generic
|
|
110
|
+
`VP-<CAT>` scheme (the harness Taxonomy-match gate FAILs on mismatch).
|
|
111
|
+
- `qa/context.md` — project-wide context set by the QA lead. Read ONCE before building the Coverage Map; apply to every screen. Extraction rules:
|
|
112
|
+
- **Roles** → for each role in the table: add to the `@auth:X` tag pool; generate a VP-SEC blocked-access scenario for every role boundary relevant to this screen.
|
|
113
|
+
- **Testing strategy → Focus areas** → if `security` listed: VP-SEC is mandatory Tier 1 for every free-text input regardless of spec risk level; if `ui` not listed: all VP-UI scenarios move to Tier 2 minimum.
|
|
114
|
+
- **Testing strategy → Mandatory coverage** → each line is a hard override applied to this screen regardless of spec risk; document in `Context constraints` of the Coverage Map.
|
|
115
|
+
- **Testing strategy → Deprioritize/skip** → record in `Context constraints`; suppress those VP categories from Tier 1 generation.
|
|
116
|
+
- **Global business rules** → add each to the `Business rules` section tagged `[G]` (e.g. `[G1 – soft-delete only]`); treat as `HIGH` risk unless stated otherwise.
|
|
117
|
+
- **Error patterns** → use as fallback only when `spec.md` does not give exact error text; never override spec-specified messages.
|
|
118
|
+
- If `qa/context.md` is absent: proceed without it — no impact on the generation flow.
|
|
108
119
|
|
|
109
120
|
**Single screen focus**: one URL = one screen. Modals on same page = part of this screen.
|
|
110
121
|
This means: do not test other screens' UI layout or navigation. It does NOT mean skip documenting business outcomes that your screen's actions cause on other surfaces. Those cross-surface outcomes must appear in the Coverage Map and be covered by at least `@manual` scenarios.
|
|
@@ -129,6 +140,11 @@ Read `spec.md` fully, then extract into a Coverage Map **before writing any scen
|
|
|
129
140
|
**Risk tags:** HIGH = complex business rules, cascading fields, multi-step state changes, auth/integration. LOW = display-only, static labels, read-only fields.
|
|
130
141
|
|
|
131
142
|
```
|
|
143
|
+
Context constraints: [populated from qa/context.md before writing any scenario]
|
|
144
|
+
roles: [list roles, e.g. admin / manager / staff]
|
|
145
|
+
strategy: [active overrides, e.g. "VP-SEC mandatory T1", "VP-UI → T2 only"]
|
|
146
|
+
global rules: [G1 – ...] → also appear in Business rules below tagged [G]
|
|
147
|
+
→ leave empty if qa/context.md is absent or has no entries applicable to this screen
|
|
132
148
|
User journeys: [J1 – ...], [J2 – ...]
|
|
133
149
|
Validation rules: [V1 – field → "exact error text"], [V2 – ...]
|
|
134
150
|
Business rules: [B1 HIGH – ...], [B2 LOW – ...]
|
|
@@ -221,7 +237,7 @@ Security: [S1 – admin only]
|
|
|
221
237
|
| **auth** | valid-login · invalid-credential · access-control |
|
|
222
238
|
|
|
223
239
|
**Required assertion shapes (use these, not bare visibility):**
|
|
224
|
-
- Card info: assert at **card level** (image+name+price together), e.g. `User see all [Product Card] contain {{...}}` — not `see [Section]
|
|
240
|
+
- Card info: assert at **card level** (image+name+price together), e.g. `User see all [Product Card] contain {{...}}` — not `see [Section]` (section-level passes even if one card lacks price).
|
|
225
241
|
- Cross-screen consistency (detail/cart): **capture then compare** —
|
|
226
242
|
```gherkin
|
|
227
243
|
When User remember [Product Name] text as {{selected_product_name}}
|
|
@@ -239,13 +255,34 @@ Security: [S1 – admin only]
|
|
|
239
255
|
- **If the spec lacks the concrete value** a deep assertion needs (exact message, price, count): still write the deep shape with a `{{var}}` placeholder and leave a `# SPEC-GAP: <field> value not in spec` comment — do **not** downgrade to `see [X] section`. A visible gap is better than a silent shallow pass.
|
|
240
256
|
- **Blind-Spot Memory:** before finishing, run `sungen blindspot list --prompt` (Bash) and make sure the suite satisfies each recorded pattern (e.g. "for any Add/Create action: check success + resulting data state + duplicate/double-submit"). These are gaps QA hit before — don't repeat them.
|
|
241
257
|
|
|
242
|
-
**First-pass anti-patterns (exactly what the gate/reviewer reject — avoid them):**
|
|
243
|
-
- Title↔steps mismatch
|
|
244
|
-
- Tautology `Then`: `click [Next Slide]` → `see [Carousel] section` (proves nothing).
|
|
245
|
-
- Business-critical scenario ending at `see [Added] modal` / `see [Cart] page` with no data assertion.
|
|
258
|
+
**First-pass anti-patterns (these are exactly what the gate/reviewer reject — avoid them):**
|
|
259
|
+
- Title↔steps mismatch: e.g. a "no-result state" scenario that clicks a query which **returns** products. Steps must create the condition the title claims.
|
|
260
|
+
- Tautology `Then`: `click [Next Slide]` → `see [Carousel] section` (always visible, proves nothing). Assert the change (new slide title differs).
|
|
261
|
+
- Business-critical scenario ending at `see [Added] modal` / `see [Cart] page` / `see [Category Products] page` with no data assertion.
|
|
246
262
|
- Brand filter covered only as navigation (must assert products belong to the brand).
|
|
247
263
|
|
|
248
|
-
**Balance:** cover all the above (deep) BEFORE expanding subscription / UI-presence / extra validation edge cases.
|
|
264
|
+
**Balance:** cover all the above (deep) BEFORE expanding subscription / UI-presence / extra validation edge cases. Do not over-invest in subscription while cart/detail/filter correctness are shallow.
|
|
265
|
+
|
|
266
|
+
#### Harness gates — satisfy on the FIRST pass (don't make the repair loop fix them)
|
|
267
|
+
|
|
268
|
+
`sungen audit` enforces these. Generate compliant output up front:
|
|
269
|
+
|
|
270
|
+
1. **Taxonomy-match** (`VP-TAXONOMY-MISMATCH`, gate-FAIL) — when `test-viewpoint.md` declares its own viewpoint IDs (e.g. `VP0`, `VP1`, … `VP12`, `MS-HP-001`, `MS-EH-001`), **reuse those IDs verbatim as the scenario codes**. Do NOT invent a generic `VP-UI / VP-LOGIC / VP-VAL` scheme — that breaks the coverage matrix. Only fall back to `VP-<CATEGORY>-<NNN>` when the viewpoint file declares no IDs.
|
|
271
|
+
2. **Spec-coverage triggers** (`TRIGGER-UNCOVERED`, gate-FAIL) — the Validation-Rules table lists a **trigger** per constraint (e.g. `blur, submit`). Generate one scenario **per (constraint × trigger)** — a `format` rule validating *on blur AND on submit* needs BOTH a blur scenario (`press Tab`) and a submit scenario (`click [Submit]` / `press Enter`). Never collapse the trigger × input matrix to one representative case.
|
|
272
|
+
3. **Claim-Proof** (`CLAIM-UNPROVEN`) — a title claiming `all`/`only`/`every`/`single`/`correct`/`same`/`changes`/`hidden`/`cleared`/`restored`/`independent`/`sanitized`/`announces` MUST have the matching assertion (`see all …`, count, `remember`+compare, `is hidden`, return-and-assert-empty, etc.). If the title promises it, the steps must prove it.
|
|
273
|
+
- **Negative / absence claims** (`does not` / `no` / `never` / `prevents` / `không` / `chưa` — any language; `no-side-effect/no-duplicate`, `negative-claim/absence`): the `Then` must **differ** between the claim holding and not holding. A terminal `see [X] page` that looks identical whether or not the bad thing happened proves nothing. For a side-effect that should NOT repeat (re-submit on back, re-charge, duplicate order, resend OTP), assert the **count is unchanged** (`User see [Records] table with {{one}}` / `row with {{count}}`); if it's not UI-observable, mark `@manual` with a request-count oracle (shape below). This is general — it covers any side-effect, not a fixed verb list.
|
|
274
|
+
4. **Downstream-scope** (`DOWNSTREAM-SCOPE-MISSING`) — when the spec's Navigation Flow / success target is **another screen** (e.g. a confirmation/sent page), don't stop at a terminal `see [X] page`. Either cover that screen's content/guards (if its viewpoint items are in scope — they often have their own `MS-*` IDs), or scaffold it (`sungen add --screen <name>`) and note the handoff. Do not silently drop the downstream surface.
|
|
275
|
+
5. **Manual-oracle** (`MANUAL-STEPS-INSUFFICIENT`) — every `@manual` scenario needs **setup · action · observable expected · oracle/tool**, not a one-line note. Use this comment shape:
|
|
276
|
+
```gherkin
|
|
277
|
+
@high @manual
|
|
278
|
+
Scenario: VP-… <claim>
|
|
279
|
+
# MANUAL: <why it can't be automated — needs network capture / inbox / screen-reader / multi-tab>
|
|
280
|
+
# Tester verifies:
|
|
281
|
+
# 1. <setup> e.g. seed a registered email; throttle the network
|
|
282
|
+
# 2. <action> e.g. click [Submit] with the request in flight
|
|
283
|
+
# 3. <observable> e.g. only ONE POST is dispatched
|
|
284
|
+
# 4. Oracle: <tool> e.g. DevTools Network panel / mail-catcher / NVDA
|
|
285
|
+
```
|
|
249
286
|
|
|
250
287
|
#### Tier 1 guard — minimum before writing scenarios
|
|
251
288
|
|
|
@@ -363,7 +400,7 @@ Add cleanup tags per the `sungen-gherkin-syntax` Cleanup table. Key rules:
|
|
|
363
400
|
**Files:** `qa/screens/<screen>/features/<screen>.feature` + `qa/screens/<screen>/test-data/<screen>.yaml`
|
|
364
401
|
|
|
365
402
|
Use step patterns and element types from `sungen-gherkin-syntax`.
|
|
366
|
-
**Naming**: `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
|
|
403
|
+
**Naming**: reuse the **project's `test-viewpoint.md` IDs** when it declares them (e.g. `VP0`, `MS-HP-001`); otherwise `VP-<CATEGORY>-<NNN>`. Scenario name must use the **same element type** as the steps.
|
|
367
404
|
|
|
368
405
|
**Test data** — grouped by section, loaded at runtime:
|
|
369
406
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
|
|
13
13
|
"build:dashboard": "cd dashboard && npm install --silent && npm run build && cd .. && cp dashboard/dist/index.html src/dashboard/templates/index.html",
|
|
14
14
|
"dev": "tsx src/cli/index.ts",
|
|
15
|
-
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts",
|
|
16
|
-
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update",
|
|
15
|
+
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts",
|
|
16
|
+
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
|
|
17
17
|
"prepublishOnly": "npm run build:dashboard && npm run build"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
@@ -58,6 +58,25 @@ function render(r: AuditReport): void {
|
|
|
58
58
|
L(` ⑥ Traceability — ${(r.trace.mappedRatio * 100).toFixed(0)}% scenarios linked to viewpoint-overview`);
|
|
59
59
|
L(` ${r.trace.note}`);
|
|
60
60
|
L('');
|
|
61
|
+
if (r.spec.hasSpec && (r.spec.frTotal > 0 || r.spec.triggerGaps.length > 0 || r.spec.verdict !== 'pass')) {
|
|
62
|
+
L(` ⑦ Spec coverage — FR ${r.spec.frCovered}/${r.spec.frTotal} covered [${r.spec.verdict.toUpperCase()}]`);
|
|
63
|
+
for (const g of r.spec.triggerGaps) L(` ✗ TRIGGER-UNCOVERED: "${g.constraint}"${g.code ? ` (${g.code})` : ''} mandated on [${g.required.join(', ')}], only tested on [${g.found.join(', ') || 'none'}] → missing ${g.missing.join(', ')}`);
|
|
64
|
+
for (const u of r.spec.uncoveredMust.slice(0, 6)) L(` ✗ SPEC-UNCOVERED: ${u.id} (MUST) — "${u.text}"`);
|
|
65
|
+
if (!r.spec.triggerGaps.length && !r.spec.uncoveredMust.length) L(' ✓ every MUST FR + per-constraint trigger covered');
|
|
66
|
+
L('');
|
|
67
|
+
}
|
|
68
|
+
if (r.ledger.hasViewpoint && r.ledger.total > 0) {
|
|
69
|
+
L(` ⑧ Viewpoint atomic coverage — ${r.ledger.covered}/${r.ledger.total} items (${(r.ledger.ratio * 100).toFixed(0)}%)`);
|
|
70
|
+
for (const m of r.ledger.missing.slice(0, 8)) L(` ○ missing: ${m.id ? m.id + ' — ' : ''}${m.text.slice(0, 70)}`);
|
|
71
|
+
if (r.ledger.missing.length > 8) L(` … +${r.ledger.missing.length - 8} more`);
|
|
72
|
+
L('');
|
|
73
|
+
}
|
|
74
|
+
if (r.calibration) {
|
|
75
|
+
const ax = Object.entries(r.calibration.axes).map(([k, v]) => `${k}=${(v * 100).toFixed(0)}%`).join(' · ');
|
|
76
|
+
L(` ⑨ Calibration — ${ax}`);
|
|
77
|
+
L(` weakest: ${r.calibration.weakest.axis} ${(r.calibration.weakest.value * 100).toFixed(0)}%${r.calibration.inflated ? ' ⚠ SCORE-INFLATED-BY-BREADTH' : ''}`);
|
|
78
|
+
L('');
|
|
79
|
+
}
|
|
61
80
|
L(' ── Findings (Repair targets) ──');
|
|
62
81
|
if (r.findings.length === 0) L(' ✓ none — output passes the harness');
|
|
63
82
|
for (const f of r.findings) L(` • ${f}`);
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
renderCsv,
|
|
21
21
|
writeCsv,
|
|
22
22
|
} from '../../exporters/csv-exporter';
|
|
23
|
-
import {
|
|
23
|
+
import { renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
|
|
24
24
|
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow } from '../../exporters/types';
|
|
25
25
|
|
|
26
26
|
const COLOR = {
|
|
@@ -421,7 +421,14 @@ async function exportTarget(
|
|
|
421
421
|
const tempSummary = buildSummary(label, rows, '');
|
|
422
422
|
const csv = renderCsv(tempSummary, rows, specLink);
|
|
423
423
|
const csvPath = writeCsv(cwd, target.featureBaseName, csv);
|
|
424
|
-
|
|
424
|
+
// XLSX: two sheets — "Auto" (automatable: Auto + Not compiled) and "Manual" (@manual) —
|
|
425
|
+
// so QA manages the automated vs manual test-case sets separately. (CSV keeps every row.)
|
|
426
|
+
const autoRows = rows.filter((r) => r.testcaseType !== 'Manual');
|
|
427
|
+
const manualRows = rows.filter((r) => r.testcaseType === 'Manual');
|
|
428
|
+
const wb = renderXlsxMultiSheet([
|
|
429
|
+
{ sheetName: 'Auto', summary: buildSummary(label, autoRows, ''), rows: autoRows, specLink },
|
|
430
|
+
{ sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
|
|
431
|
+
]);
|
|
425
432
|
await writeXlsx(cwd, target.featureBaseName, wb);
|
|
426
433
|
return buildSummary(label, rows, path.relative(cwd, csvPath));
|
|
427
434
|
}
|
|
@@ -429,7 +436,12 @@ async function exportTarget(
|
|
|
429
436
|
const variants = discoverLocaleVariants(cwd, target);
|
|
430
437
|
let primarySummary: ScreenSummary | null = null;
|
|
431
438
|
let primaryCsvPath = '';
|
|
432
|
-
|
|
439
|
+
// XLSX is split by automation type: an "Auto" sheet (automatable TCs, results differ per
|
|
440
|
+
// locale) and a single shared "Manual" sheet (@manual TCs don't execute and are the same
|
|
441
|
+
// across locales). With multiple locales, Auto sheets are prefixed by locale code.
|
|
442
|
+
const autoSheets: { sheetName: string; summary: ScreenSummary; rows: TestCaseRow[]; specLink: string }[] = [];
|
|
443
|
+
let manualRows: TestCaseRow[] = [];
|
|
444
|
+
const multiLocale = variants.length > 1;
|
|
433
445
|
|
|
434
446
|
for (const variant of variants) {
|
|
435
447
|
// For the base variant the overlay merge is skipped (`locale: null`);
|
|
@@ -446,33 +458,37 @@ async function exportTarget(
|
|
|
446
458
|
env,
|
|
447
459
|
selectorKeyMap,
|
|
448
460
|
});
|
|
449
|
-
const variantSummary = buildSummary(label, variantRows, '');
|
|
450
461
|
|
|
451
|
-
// CSV: always one file per locale (CSV has no sheet concept).
|
|
462
|
+
// CSV: always one file per locale, every row (CSV has no sheet concept).
|
|
452
463
|
const csvLocale = variant.locale || null; // '' or 'en' → '' / 'en'
|
|
453
|
-
const csv = renderCsv(
|
|
464
|
+
const csv = renderCsv(buildSummary(label, variantRows, ''), variantRows, specLink);
|
|
454
465
|
const csvPath = writeCsv(cwd, target.featureBaseName, csv, csvLocale);
|
|
455
466
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
467
|
+
const autoRows = variantRows.filter((r) => r.testcaseType !== 'Manual');
|
|
468
|
+
autoSheets.push({
|
|
469
|
+
sheetName: multiLocale ? `${variant.displayCode} Auto` : 'Auto',
|
|
470
|
+
summary: buildSummary(label, autoRows, ''),
|
|
471
|
+
rows: autoRows,
|
|
460
472
|
specLink,
|
|
461
473
|
});
|
|
462
474
|
|
|
463
|
-
// Use the base variant
|
|
464
|
-
// top-level reporter rolls up base-locale numbers.
|
|
475
|
+
// Use the base variant for the shared Manual sheet + the rolled-up "primary" summary.
|
|
465
476
|
if (variant.locale === '') {
|
|
477
|
+
manualRows = variantRows.filter((r) => r.testcaseType === 'Manual');
|
|
466
478
|
primarySummary = buildSummary(label, variantRows, path.relative(cwd, csvPath));
|
|
467
479
|
primaryCsvPath = csvPath;
|
|
468
480
|
}
|
|
469
481
|
}
|
|
470
482
|
|
|
471
|
-
//
|
|
472
|
-
const
|
|
483
|
+
// All Auto sheets, then one "Manual" sheet — always present for a predictable structure.
|
|
484
|
+
const sheets = [
|
|
485
|
+
...autoSheets,
|
|
486
|
+
{ sheetName: 'Manual', summary: buildSummary(label, manualRows, ''), rows: manualRows, specLink },
|
|
487
|
+
];
|
|
488
|
+
const wb = renderXlsxMultiSheet(sheets);
|
|
473
489
|
await writeXlsx(cwd, target.featureBaseName, wb);
|
|
474
490
|
|
|
475
|
-
return primarySummary ?? buildSummary(label,
|
|
491
|
+
return primarySummary ?? buildSummary(label, (autoSheets[0]?.rows ?? []).concat(manualRows), primaryCsvPath);
|
|
476
492
|
} catch (err) {
|
|
477
493
|
console.error(`${COLOR.red}Error exporting ${label}:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
|
|
478
494
|
return null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { lintSkills, defaultSkillDir } from '../../harness/eval/skill-lint';
|
|
3
|
+
|
|
4
|
+
export function registerEvalCommand(program: Command): void {
|
|
5
|
+
program
|
|
6
|
+
.command('eval')
|
|
7
|
+
.description('Eval harness: quality checks on Sungen\'s own skills/instructions (dev/CI)')
|
|
8
|
+
.option('--skills', 'Static skill-lint: frontmatter, line budget, claude↔github sync, registration')
|
|
9
|
+
.option('--dir <path>', 'Templates dir to lint (default: bundled ai-instructions)')
|
|
10
|
+
.option('--json', 'Output the raw findings JSON')
|
|
11
|
+
.action((options) => {
|
|
12
|
+
try {
|
|
13
|
+
if (!options.skills) throw new Error('Provide --skills (the only eval mode today)');
|
|
14
|
+
const dir = options.dir || defaultSkillDir();
|
|
15
|
+
const r = lintSkills(dir);
|
|
16
|
+
if (options.json) { console.log(JSON.stringify(r, null, 2)); process.exit(r.errors > 0 ? 2 : 0); }
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log(`━━━ Skill-lint: ${r.checked} skill template(s) ━━━`);
|
|
19
|
+
if (!r.findings.length) console.log(' ✓ all skills pass (frontmatter · line-budget · variant-sync · registration)');
|
|
20
|
+
for (const f of r.findings) console.log(` ${f.level === 'error' ? '✗' : '⚠'} [${f.rule}] ${f.file} — ${f.detail}`);
|
|
21
|
+
console.log('');
|
|
22
|
+
process.exit(r.errors > 0 ? 2 : 0);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { parseLegacyFile, listSheets, baselineAudit, BaselineReport, LegacyInventory, inventoryToGherkin, reconcileViewpoints, renderViewpointOverview, fetchGoogleSheet } from '../../ingest';
|
|
5
|
+
|
|
6
|
+
function renderInventoryMd(inv: LegacyInventory, r: BaselineReport): string {
|
|
7
|
+
const lines: string[] = [];
|
|
8
|
+
lines.push(`# Legacy Testcase Inventory — ${inv.source.file}`, '');
|
|
9
|
+
lines.push(`Total testcases: **${r.total}**`, '');
|
|
10
|
+
lines.push('## Sheets', '', '| Sheet | Type | Rows |', '|---|---|---|');
|
|
11
|
+
for (const s of r.sheets) lines.push(`| ${s.name} | ${s.type} | ${s.rows} |`);
|
|
12
|
+
lines.push('', '## By category', '');
|
|
13
|
+
for (const [k, v] of Object.entries(r.byCategory).sort((a, b) => b[1] - a[1])) lines.push(`- ${k}: ${v}`);
|
|
14
|
+
lines.push('', `Depth: **${(r.depthRatio * 100).toFixed(0)}%** (${r.deepCount}/${r.total} assert a concrete expected value)`, '');
|
|
15
|
+
return lines.join('\n');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function render(inv: LegacyInventory, r: BaselineReport): void {
|
|
19
|
+
const L = console.log;
|
|
20
|
+
const pct = (x: number) => (r.total ? Math.round((x / r.total) * 100) : 0);
|
|
21
|
+
L('');
|
|
22
|
+
L(`━━━ Legacy Ingest — ${inv.source.file} ━━━`);
|
|
23
|
+
L('');
|
|
24
|
+
L(` Sheets:`);
|
|
25
|
+
for (const s of r.sheets) L(` • ${s.name} — ${s.type} (${s.rows})`);
|
|
26
|
+
L('');
|
|
27
|
+
L(` TOTAL testcases: ${r.total}`);
|
|
28
|
+
L('');
|
|
29
|
+
L(' ── QA baseline ──');
|
|
30
|
+
L(` By category: ${Object.entries(r.byCategory).sort((a, b) => b[1] - a[1]).slice(0, 12).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
31
|
+
L(` By priority: ${Object.entries(r.byPriority).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
32
|
+
L(` By result: ${Object.entries(r.byResult).map(([k, v]) => `${k}=${v}`).join(' ')}`);
|
|
33
|
+
L(` Depth: ${r.deepCount}/${r.total} (${(r.depthRatio * 100).toFixed(0)}%) assert a concrete expected value → convert to DEEP Gherkin`);
|
|
34
|
+
L(` Duplicates: ${r.duplicateClusters} same-shape cluster(s), ${r.exactDuplicates} likely exact`);
|
|
35
|
+
L('');
|
|
36
|
+
L(' ── Capability Plan (which driver, if any) ──');
|
|
37
|
+
L(` UI automatable : ${r.reasons.ui} (${pct(r.reasons.ui)}%)`);
|
|
38
|
+
L(` cross-screen → flow : ${r.reasons.crossScreen} (${pct(r.reasons.crossScreen)}%)`);
|
|
39
|
+
L(` capability-manual : ${r.reasons.capabilityManual} (${pct(r.reasons.capabilityManual)}%)`);
|
|
40
|
+
L(` keep-manual : ${r.reasons.keepManual} (${pct(r.reasons.keepManual)}%)`);
|
|
41
|
+
if (r.reasons.driverCandidates.length)
|
|
42
|
+
L(` driver candidates : ${r.reasons.driverCandidates.map((d) => `${d.driver}×${d.count}`).join(' ')}`);
|
|
43
|
+
else
|
|
44
|
+
L(` driver candidates : none (no capability-manual at scale → no driver justified)`);
|
|
45
|
+
L('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function registerIngestCommand(program: Command): void {
|
|
49
|
+
program
|
|
50
|
+
.command('ingest')
|
|
51
|
+
.description('Ingest a legacy manual testcase workbook (CSV/XLSX/JSON-bundle) → normalized inventory + QA baseline audit')
|
|
52
|
+
.option('--legacy <file...>', 'Path(s) to the legacy testcase file(s): .csv, .xlsx, or a .json sheet-bundle')
|
|
53
|
+
.option('--gsheet <urlOrId>', 'Fetch a Google Sheet (all tabs) under YOUR Google identity (ADC, read-only) → bundle. Needs: npm i googleapis + gcloud auth application-default login')
|
|
54
|
+
.option('-s, --screen <name>', 'Screen name (output goes under qa/screens/<name>/requirements/legacy/)')
|
|
55
|
+
.option('--out <dir>', 'Output directory (overrides the default screen path)')
|
|
56
|
+
.option('--sheets <names>', 'Comma-separated tab names to ingest (default: all). Workbooks carry many tabs.')
|
|
57
|
+
.option('--list-sheets', 'List the tabs + detected type (testcase/viewpoint-matrix/ui-checklist) and exit')
|
|
58
|
+
.option('--emit-gherkin', 'Also emit a traceable Gherkin DRAFT (.legacy-draft.feature) + trace map (P-B)')
|
|
59
|
+
.option('--json', 'Print the raw inventory + baseline JSON')
|
|
60
|
+
.action(async (options) => {
|
|
61
|
+
try {
|
|
62
|
+
if (!options.legacy && !options.gsheet) throw new Error('Provide --legacy <file...> or --gsheet <url|id>');
|
|
63
|
+
|
|
64
|
+
const outDir = options.out
|
|
65
|
+
? path.resolve(process.cwd(), options.out)
|
|
66
|
+
: options.screen
|
|
67
|
+
? path.join(process.cwd(), 'qa', 'screens', options.screen, 'requirements', 'legacy')
|
|
68
|
+
: path.join(process.cwd(), '.sungen', 'legacy');
|
|
69
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
let files: string[];
|
|
72
|
+
if (options.gsheet) {
|
|
73
|
+
// Fetch as the user (not AI) — bypasses the AI-context DLP legitimately.
|
|
74
|
+
const bundle = await fetchGoogleSheet(String(options.gsheet));
|
|
75
|
+
const bundlePath = path.join(outDir, 'bundle.json');
|
|
76
|
+
fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 0));
|
|
77
|
+
console.log(` Fetched Google Sheet "${bundle.source}" → ${bundle.sheets.length} tab(s) → ${path.relative(process.cwd(), bundlePath)}`);
|
|
78
|
+
files = [bundlePath];
|
|
79
|
+
} else {
|
|
80
|
+
files = (Array.isArray(options.legacy) ? options.legacy : [options.legacy])
|
|
81
|
+
.map((f: string) => path.resolve(process.cwd(), f));
|
|
82
|
+
for (const f of files) if (!fs.existsSync(f)) throw new Error(`File not found: ${f}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (options.listSheets) {
|
|
86
|
+
const sheets = await listSheets(files);
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(' Tabs found:');
|
|
89
|
+
for (const s of sheets) console.log(` • ${s.name} — ${s.type} (${s.rows} rows)`);
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(` Ingest the testcase tab(s) with: --sheets "${sheets.filter((s) => s.type === 'testcase').map((s) => s.name).join(',')}"`);
|
|
92
|
+
console.log('');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const onlySheets: string[] | undefined = options.sheets ? String(options.sheets).split(',') : undefined;
|
|
97
|
+
const inv = await parseLegacyFile(files, onlySheets);
|
|
98
|
+
const report = baselineAudit(inv);
|
|
99
|
+
fs.writeFileSync(path.join(outDir, 'inventory.json'), JSON.stringify({ inventory: inv, baseline: report }, null, 2));
|
|
100
|
+
fs.writeFileSync(path.join(outDir, 'inventory.md'), renderInventoryMd(inv, report));
|
|
101
|
+
|
|
102
|
+
let convert;
|
|
103
|
+
let recon;
|
|
104
|
+
if (options.emitGherkin) {
|
|
105
|
+
const featureName = options.screen || (files.length === 1 ? path.basename(files[0]).replace(/\.[^.]+$/, '') : 'legacy');
|
|
106
|
+
convert = inventoryToGherkin(inv, featureName);
|
|
107
|
+
fs.writeFileSync(path.join(outDir, `${featureName}.legacy-draft.feature`), convert.feature);
|
|
108
|
+
fs.writeFileSync(path.join(outDir, 'legacy-trace.json'), JSON.stringify(convert.trace, null, 2));
|
|
109
|
+
// Viewpoint reconciliation: legacy coverage vs catalog → blind-spots (P-C)
|
|
110
|
+
recon = reconcileViewpoints(inv);
|
|
111
|
+
fs.writeFileSync(path.join(outDir, 'test-viewpoint.draft.md'), renderViewpointOverview(featureName, recon));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (options.json) console.log(JSON.stringify({ inventory: inv, baseline: report, convert, reconciliation: recon }, null, 2));
|
|
115
|
+
else {
|
|
116
|
+
render(inv, report);
|
|
117
|
+
if (convert) {
|
|
118
|
+
const g = convert.gap;
|
|
119
|
+
console.log(' ── Gherkin draft (P-B) ──');
|
|
120
|
+
console.log(` ${g.total} scenario(s) drafted → ui=${g.ui} · cross-screen=${g.crossScreen} · @manual(capability)=${g.manualCapability} · @manual(keep)=${g.manualKeep}`);
|
|
121
|
+
if (g.noExpected) console.log(` ⚠ ${g.noExpected} testcase(s) without an Expected → needs review`);
|
|
122
|
+
console.log(` Draft: ${path.relative(process.cwd(), outDir)}/*.legacy-draft.feature (refine via /sungen:create-test)`);
|
|
123
|
+
}
|
|
124
|
+
if (recon) {
|
|
125
|
+
console.log(' ── Viewpoint reconciliation (legacy vs catalog) ──');
|
|
126
|
+
console.log(` page-type: ${recon.pageType ?? 'unknown'} · legacy covers ${recon.themesCovered}/${recon.themesTotal} catalog themes (${(recon.coverageRatio * 100).toFixed(0)}%)`);
|
|
127
|
+
if (recon.blindSpots.length)
|
|
128
|
+
console.log(` ⚠ BLIND-SPOTS (catalog expects, legacy lacks): ${recon.blindSpots.map((b) => `${b.theme}[${b.status}]`).join(', ')}`);
|
|
129
|
+
else
|
|
130
|
+
console.log(` ✓ no catalog blind-spots — legacy covers the expected themes`);
|
|
131
|
+
console.log(` Draft viewpoint: ${path.relative(process.cwd(), outDir)}/test-viewpoint.draft.md → seed /sungen:create-test`);
|
|
132
|
+
}
|
|
133
|
+
console.log(` Inventory: ${path.relative(process.cwd(), outDir)}/inventory.json`);
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|