@sun-asterisk/sungen 3.0.0-beta.74 → 3.0.0-beta.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +17 -3
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/delivery.d.ts.map +1 -1
  5. package/dist/cli/commands/delivery.js +30 -14
  6. package/dist/cli/commands/delivery.js.map +1 -1
  7. package/dist/cli/commands/ingest.d.ts +3 -0
  8. package/dist/cli/commands/ingest.d.ts.map +1 -0
  9. package/dist/cli/commands/ingest.js +179 -0
  10. package/dist/cli/commands/ingest.js.map +1 -0
  11. package/dist/cli/index.js +2 -0
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/dashboard/templates/index.html +54 -54
  14. package/dist/generators/gherkin-parser/index.d.ts +2 -0
  15. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  16. package/dist/generators/gherkin-parser/index.js +15 -0
  17. package/dist/generators/gherkin-parser/index.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
  19. package/dist/generators/test-generator/patterns/capture-patterns.d.ts +5 -0
  20. package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +1 -1
  21. package/dist/generators/test-generator/patterns/capture-patterns.js +33 -0
  22. package/dist/generators/test-generator/patterns/capture-patterns.js.map +1 -1
  23. package/dist/harness/audit.d.ts +5 -1
  24. package/dist/harness/audit.d.ts.map +1 -1
  25. package/dist/harness/audit.js +13 -2
  26. package/dist/harness/audit.js.map +1 -1
  27. package/dist/harness/capability-plan.d.ts +6 -0
  28. package/dist/harness/capability-plan.d.ts.map +1 -1
  29. package/dist/harness/capability-plan.js +13 -0
  30. package/dist/harness/capability-plan.js.map +1 -1
  31. package/dist/harness/parse.d.ts +1 -0
  32. package/dist/harness/parse.d.ts.map +1 -1
  33. package/dist/harness/parse.js +3 -0
  34. package/dist/harness/parse.js.map +1 -1
  35. package/dist/harness/provenance.d.ts +6 -0
  36. package/dist/harness/provenance.d.ts.map +1 -0
  37. package/dist/harness/provenance.js +65 -0
  38. package/dist/harness/provenance.js.map +1 -0
  39. package/dist/harness/sensors.d.ts +30 -0
  40. package/dist/harness/sensors.d.ts.map +1 -1
  41. package/dist/harness/sensors.js +122 -0
  42. package/dist/harness/sensors.js.map +1 -1
  43. package/dist/ingest/baseline-audit.d.ts +38 -0
  44. package/dist/ingest/baseline-audit.d.ts.map +1 -0
  45. package/dist/ingest/baseline-audit.js +85 -0
  46. package/dist/ingest/baseline-audit.js.map +1 -0
  47. package/dist/ingest/gsheet-fetch.d.ts +9 -0
  48. package/dist/ingest/gsheet-fetch.d.ts.map +1 -0
  49. package/dist/ingest/gsheet-fetch.js +180 -0
  50. package/dist/ingest/gsheet-fetch.js.map +1 -0
  51. package/dist/ingest/index.d.ts +6 -0
  52. package/dist/ingest/index.d.ts.map +1 -0
  53. package/dist/ingest/index.js +22 -0
  54. package/dist/ingest/index.js.map +1 -0
  55. package/dist/ingest/legacy-parser.d.ts +39 -0
  56. package/dist/ingest/legacy-parser.d.ts.map +1 -0
  57. package/dist/ingest/legacy-parser.js +218 -0
  58. package/dist/ingest/legacy-parser.js.map +1 -0
  59. package/dist/ingest/reconcile.d.ts +30 -0
  60. package/dist/ingest/reconcile.d.ts.map +1 -0
  61. package/dist/ingest/reconcile.js +65 -0
  62. package/dist/ingest/reconcile.js.map +1 -0
  63. package/dist/ingest/to-gherkin.d.ts +33 -0
  64. package/dist/ingest/to-gherkin.d.ts.map +1 -0
  65. package/dist/ingest/to-gherkin.js +93 -0
  66. package/dist/ingest/to-gherkin.js.map +1 -0
  67. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  68. package/dist/orchestrator/ai-rules-updater.js +2 -0
  69. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  70. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
  71. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
  72. package/dist/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
  73. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
  74. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
  75. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
  76. package/package.json +3 -3
  77. package/src/cli/commands/audit.ts +13 -3
  78. package/src/cli/commands/delivery.ts +31 -15
  79. package/src/cli/commands/ingest.ts +141 -0
  80. package/src/cli/index.ts +2 -0
  81. package/src/dashboard/templates/index.html +54 -54
  82. package/src/generators/gherkin-parser/index.ts +17 -0
  83. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/all-contain-element.hbs +8 -0
  84. package/src/generators/test-generator/patterns/capture-patterns.ts +38 -0
  85. package/src/harness/audit.ts +18 -4
  86. package/src/harness/capability-plan.ts +11 -0
  87. package/src/harness/parse.ts +4 -0
  88. package/src/harness/provenance.ts +33 -0
  89. package/src/harness/sensors.ts +189 -0
  90. package/src/ingest/baseline-audit.ts +100 -0
  91. package/src/ingest/gsheet-fetch.ts +152 -0
  92. package/src/ingest/index.ts +5 -0
  93. package/src/ingest/legacy-parser.ts +184 -0
  94. package/src/ingest/reconcile.ts +80 -0
  95. package/src/ingest/to-gherkin.ts +108 -0
  96. package/src/orchestrator/ai-rules-updater.ts +2 -0
  97. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +10 -0
  98. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +12 -0
  99. package/src/orchestrator/templates/ai-instructions/claude-skill-ingest-legacy.md +79 -0
  100. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +10 -0
  101. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +12 -0
  102. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-ingest-legacy.md +79 -0
@@ -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.
@@ -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
@@ -74,6 +74,18 @@ User switch to [T] frame | [main] frame
74
74
  # 8. Page: User see [T] page
75
75
  ```
76
76
 
77
+ ### Collection / all-card (P5)
78
+
79
+ ```
80
+ # Every element's TEXT matches a value (filter correctness, all items belong):
81
+ User see all [Result Product Name] text contains {{category_term}}
82
+ # Every CONTAINER holds a CHILD element (structural per-card proof — prove "each card has X"):
83
+ User see all [Product Card] contain [Product Name]
84
+ User see all [Product Card] contain [Add To Cart] button
85
+ ```
86
+
87
+ Use the all-card form whenever a title claims *every / each* card/row exposes something — a single `User see [Add To Cart] button` does NOT prove "each card" and the harness Claim-Proof gate will flag it.
88
+
77
89
  ### Table
78
90
 
79
91
  ```
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "3.0.0-beta.74",
3
+ "version": "3.0.0-beta.77",
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",
16
- "test:update": "tsx tests/golden/run.ts --update",
15
+ "test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/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": [
@@ -23,8 +23,10 @@ function render(r: AuditReport): void {
23
23
  L('');
24
24
  L(` Quality score (business-weighted): ${r.score.overall}/10 [Viewpoint Gate: ${r.gateStatus}]`);
25
25
  L(` Intent: focus=${r.intent.focus} · depth threshold ${r.depth.threshold.toFixed(2)} (${r.intent.source})`);
26
+ L(` Provenance: sungen ${r.provenance.sungenVersion} · catalog ${r.provenance.catalogHash} (same here on every machine → same score)`);
26
27
  L(` coverage ${bar(r.score.coverage)} ${(r.score.coverage * 100).toFixed(0)}%`);
27
28
  L(` businessDepth ${bar(r.score.businessDepth)} ${(r.score.businessDepth * 100).toFixed(0)}% [${r.depth.verdict.toUpperCase()}]`);
29
+ L(` claimProof ${bar(r.claim.ratio)} ${(r.claim.ratio * 100).toFixed(0)}% [${r.claim.verdict.toUpperCase()}]`);
28
30
  L(` balance ${bar(r.score.balance)} ${(r.score.balance * 100).toFixed(0)}%`);
29
31
  L(` traceability ${bar(r.score.traceability)} ${(r.score.traceability * 100).toFixed(0)}%`);
30
32
  L(` (${r.score.formula})`);
@@ -37,15 +39,23 @@ function render(r: AuditReport): void {
37
39
  for (const s of r.depth.shallowBusinessCritical.slice(0, 8)) L(` ⚠ ${s.category}: ${s.name}`);
38
40
  if (r.depth.shallowBusinessCritical.length > 8) L(` … +${r.depth.shallowBusinessCritical.length - 8} more`);
39
41
  L('');
40
- L(` ③ Coverage balance — ${r.balance.imbalanced ? '⚠ IMBALANCED' : '✓ balanced'}`);
42
+ L(` ③ Claim-Proof — ${r.claim.proven}/${r.claim.withClaims} titled claims proven by steps [${r.claim.verdict.toUpperCase()}]`);
43
+ if (r.claim.unproven.length === 0) L(' ✓ every claim (all/only/single/correct/changes/hidden) is backed by an assertion');
44
+ for (const u of r.claim.unproven.slice(0, 8)) L(` ${u.severity === 'fail' ? '✗' : '⚠'} [${u.claim}] ${u.name} → needs ${u.need}`);
45
+ if (r.claim.unproven.length > 8) L(` … +${r.claim.unproven.length - 8} more`);
46
+ L('');
47
+ L(` ③·5 Taxonomy — ${r.taxonomy.mislabeled.length === 0 ? '✓ VP codes match scenario semantics' : `⚠ ${r.taxonomy.mislabeled.length} mislabel(s)`}`);
48
+ for (const m of r.taxonomy.mislabeled.slice(0, 6)) L(` ⚠ ${m.name} → VP-${m.current} reads as ${m.suggested} ("${m.signal}")`);
49
+ L('');
50
+ L(` ④ Coverage balance — ${r.balance.imbalanced ? '⚠ IMBALANCED' : '✓ balanced'}`);
41
51
  L(` buckets: ${Object.entries(r.balance.byBucket).map(([k, v]) => `${k}=${v}`).join(' ')}`);
42
52
  L('');
43
- L(` Duplicate — ${r.duplicates.clusters.length} same-shape cluster(s), ${r.duplicates.exactDuplicateCount} likely exact dup(s)`);
53
+ L(` Duplicate — ${r.duplicates.clusters.length} same-shape cluster(s), ${r.duplicates.exactDuplicateCount} likely exact dup(s)`);
44
54
  for (const c of r.duplicates.clusters.slice(0, 3)) {
45
55
  L(` ${c.sameDataLikely ? '✗ exact' : '○ EP/data family'} (${c.scenarios.length}): ${c.scenarios.slice(0, 3).join(' | ')}${c.scenarios.length > 3 ? ' …' : ''}`);
46
56
  }
47
57
  L('');
48
- L(` Traceability — ${(r.trace.mappedRatio * 100).toFixed(0)}% scenarios linked to viewpoint-overview`);
58
+ L(` Traceability — ${(r.trace.mappedRatio * 100).toFixed(0)}% scenarios linked to viewpoint-overview`);
49
59
  L(` ${r.trace.note}`);
50
60
  L('');
51
61
  L(' ── Findings (Repair targets) ──');
@@ -20,7 +20,7 @@ import {
20
20
  renderCsv,
21
21
  writeCsv,
22
22
  } from '../../exporters/csv-exporter';
23
- import { renderXlsx, renderXlsxMultiSheet, writeXlsx } from '../../exporters/xlsx-exporter';
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
- const wb = renderXlsx(tempSummary, rows, specLink);
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
- const sheets: { sheetName: string; summary: ScreenSummary; rows: TestCaseRow[]; specLink: string }[] = [];
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(variantSummary, variantRows, specLink);
464
+ const csv = renderCsv(buildSummary(label, variantRows, ''), variantRows, specLink);
454
465
  const csvPath = writeCsv(cwd, target.featureBaseName, csv, csvLocale);
455
466
 
456
- sheets.push({
457
- sheetName: `${target.featureBaseName}-${variant.displayCode}`,
458
- summary: variantSummary,
459
- rows: variantRows,
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's summary as the "primary" return value so the
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
- // XLSX: single-sheet when only base, multi-sheet when 2+ locales found.
472
- const wb = sheets.length >= 2 ? renderXlsxMultiSheet(sheets) : renderXlsx(sheets[0].summary, sheets[0].rows, specLink);
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, sheets[0].rows, primaryCsvPath);
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,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
+ }
package/src/cli/index.ts CHANGED
@@ -15,6 +15,7 @@ import { registerFigmaCommand } from './commands/figma';
15
15
  import { registerAddFlowCommand } from './commands/add-flow';
16
16
  import { registerDashboardCommand } from './commands/dashboard';
17
17
  import { registerAuditCommand } from './commands/audit';
18
+ import { registerIngestCommand } from './commands/ingest';
18
19
  import { registerManifestCommand } from './commands/manifest';
19
20
  import { registerLedgerCommand } from './commands/ledger';
20
21
  import { registerFeedbackCommand } from './commands/feedback';
@@ -60,6 +61,7 @@ async function main() {
60
61
  registerBlindspotCommand(program);
61
62
  registerCapabilityCommand(program);
62
63
  registerFlowCheckCommand(program);
64
+ registerIngestCommand(program);
63
65
 
64
66
  await program.parseAsync(process.argv);
65
67
  }