@sun-asterisk/sungen 3.2.33 → 3.2.34-beta.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 +4 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +54 -10
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +5 -1
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/changed.d.ts.map +1 -1
- package/dist/cli/commands/changed.js +2 -3
- package/dist/cli/commands/changed.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +2 -28
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/defect.d.ts +7 -0
- package/dist/cli/commands/defect.d.ts.map +1 -0
- package/dist/cli/commands/defect.js +68 -0
- package/dist/cli/commands/defect.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +75 -42
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/results-merge.d.ts +9 -0
- package/dist/cli/commands/results-merge.d.ts.map +1 -1
- package/dist/cli/commands/results-merge.js +7 -0
- package/dist/cli/commands/results-merge.js.map +1 -1
- package/dist/cli/commands/select.d.ts +12 -0
- package/dist/cli/commands/select.d.ts.map +1 -0
- package/dist/cli/commands/select.js +122 -0
- package/dist/cli/commands/select.js.map +1 -0
- package/dist/cli/commands/triage.d.ts +7 -0
- package/dist/cli/commands/triage.d.ts.map +1 -0
- package/dist/cli/commands/triage.js +98 -0
- package/dist/cli/commands/triage.js.map +1 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +3 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/commands/viewpoint.d.ts +7 -0
- package/dist/cli/commands/viewpoint.d.ts.map +1 -0
- package/dist/cli/commands/viewpoint.js +70 -0
- package/dist/cli/commands/viewpoint.js.map +1 -0
- package/dist/cli/index.js +13 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/dashboard/types.d.ts +1 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.d.ts +4 -0
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +12 -11
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/feature-parser.d.ts +8 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +12 -0
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +7 -0
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/manual-hybrid-oracle.d.ts +54 -0
- package/dist/exporters/manual-hybrid-oracle.d.ts.map +1 -0
- package/dist/exporters/manual-hybrid-oracle.js +114 -0
- package/dist/exporters/manual-hybrid-oracle.js.map +1 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +55 -12
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.d.ts +19 -0
- package/dist/exporters/matrix/carry-manual-results.d.ts.map +1 -1
- package/dist/exporters/matrix/carry-manual-results.js +76 -6
- package/dist/exporters/matrix/carry-manual-results.js.map +1 -1
- package/dist/exporters/matrix/export.d.ts +8 -2
- package/dist/exporters/matrix/export.d.ts.map +1 -1
- package/dist/exporters/matrix/export.js +15 -3
- package/dist/exporters/matrix/export.js.map +1 -1
- package/dist/exporters/matrix/gates.js +6 -0
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/render-csv.js +1 -1
- package/dist/exporters/matrix/render-csv.js.map +1 -1
- package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -1
- package/dist/exporters/matrix/render-testcase-sheet.js +10 -4
- package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -1
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +3 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/types.d.ts +13 -1
- package/dist/exporters/matrix/types.d.ts.map +1 -1
- package/dist/exporters/matrix/types.js.map +1 -1
- package/dist/exporters/matrix/wording.d.ts +1 -1
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +10 -1
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/playwright-report-parser.d.ts.map +1 -1
- package/dist/exporters/playwright-report-parser.js +1 -0
- package/dist/exporters/playwright-report-parser.js.map +1 -1
- package/dist/exporters/run-environment.d.ts +32 -0
- package/dist/exporters/run-environment.d.ts.map +1 -0
- package/dist/exporters/run-environment.js +125 -0
- package/dist/exporters/run-environment.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +18 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +49 -3
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts +0 -4
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +0 -7
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/exporters/types.d.ts +5 -0
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +11 -4
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +49 -19
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +4 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +23 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +3 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +3 -1
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/harness/audit.d.ts +3 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +48 -15
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/counted-assertions.d.ts +2 -0
- package/dist/harness/counted-assertions.d.ts.map +1 -0
- package/dist/harness/counted-assertions.js +38 -0
- package/dist/harness/counted-assertions.js.map +1 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +69 -27
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/defect-draft.d.ts +14 -0
- package/dist/harness/defect-draft.d.ts.map +1 -0
- package/dist/harness/defect-draft.js +143 -0
- package/dist/harness/defect-draft.js.map +1 -0
- package/dist/harness/defects.d.ts +24 -0
- package/dist/harness/defects.d.ts.map +1 -0
- package/dist/harness/defects.js +118 -0
- package/dist/harness/defects.js.map +1 -0
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/next-step.d.ts +2 -4
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +50 -5
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +45 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +142 -24
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +6 -4
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/read-text.d.ts +15 -0
- package/dist/harness/read-text.d.ts.map +1 -1
- package/dist/harness/read-text.js +25 -0
- package/dist/harness/read-text.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +7 -1
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/select-by-ids.d.ts +31 -0
- package/dist/harness/select-by-ids.d.ts.map +1 -0
- package/dist/harness/select-by-ids.js +106 -0
- package/dist/harness/select-by-ids.js.map +1 -0
- package/dist/harness/sensors.d.ts +8 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +92 -21
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-branches.js +1 -1
- package/dist/harness/spec-branches.js.map +1 -1
- package/dist/harness/triage-signals.d.ts +20 -0
- package/dist/harness/triage-signals.d.ts.map +1 -0
- package/dist/harness/triage-signals.js +30 -0
- package/dist/harness/triage-signals.js.map +1 -0
- package/dist/harness/triage-unit.d.ts +23 -0
- package/dist/harness/triage-unit.d.ts.map +1 -0
- package/dist/harness/triage-unit.js +142 -0
- package/dist/harness/triage-unit.js.map +1 -0
- package/dist/harness/triage.d.ts +28 -0
- package/dist/harness/triage.d.ts.map +1 -0
- package/dist/harness/triage.js +99 -0
- package/dist/harness/triage.js.map +1 -0
- package/dist/harness/ui-inventory.d.ts +40 -0
- package/dist/harness/ui-inventory.d.ts.map +1 -0
- package/dist/harness/ui-inventory.js +191 -0
- package/dist/harness/ui-inventory.js.map +1 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +7 -1
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-draft-header.d.ts +7 -0
- package/dist/harness/viewpoint-draft-header.d.ts.map +1 -0
- package/dist/harness/viewpoint-draft-header.js +100 -0
- package/dist/harness/viewpoint-draft-header.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts +13 -3
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +104 -18
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/harness/viewpoint-mode.d.ts +9 -0
- package/dist/harness/viewpoint-mode.d.ts.map +1 -0
- package/dist/harness/viewpoint-mode.js +52 -0
- package/dist/harness/viewpoint-mode.js.map +1 -0
- package/dist/harness/viewpoint-priority.d.ts +19 -0
- package/dist/harness/viewpoint-priority.d.ts.map +1 -0
- package/dist/harness/viewpoint-priority.js +36 -0
- package/dist/harness/viewpoint-priority.js.map +1 -0
- package/dist/harness/viewpoint-review-open.d.ts +6 -0
- package/dist/harness/viewpoint-review-open.d.ts.map +1 -0
- package/dist/harness/viewpoint-review-open.js +90 -0
- package/dist/harness/viewpoint-review-open.js.map +1 -0
- package/dist/harness/viewpoint-scan-refs.d.ts +50 -0
- package/dist/harness/viewpoint-scan-refs.d.ts.map +1 -0
- package/dist/harness/viewpoint-scan-refs.js +200 -0
- package/dist/harness/viewpoint-scan-refs.js.map +1 -0
- package/dist/harness/viewpoint-scan.d.ts +41 -0
- package/dist/harness/viewpoint-scan.d.ts.map +1 -0
- package/dist/harness/viewpoint-scan.js +118 -0
- package/dist/harness/viewpoint-scan.js.map +1 -0
- package/dist/orchestrator/flow-manager.d.ts +0 -1
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +3 -52
- package/dist/orchestrator/flow-manager.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +8 -62
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/agents/discovery.md +11 -3
- package/dist/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +32 -24
- package/dist/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +78 -15
- package/dist/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
- package/dist/orchestrator/templates/playwright.config.ts +4 -2
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +24 -2
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +23 -2
- package/dist/orchestrator/trace-mode-install.d.ts +14 -0
- package/dist/orchestrator/trace-mode-install.d.ts.map +1 -0
- package/dist/orchestrator/trace-mode-install.js +132 -0
- package/dist/orchestrator/trace-mode-install.js.map +1 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +23 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js +48 -0
- package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -0
- package/dist/orchestrator/viewpoint-scaffold.d.ts +3 -0
- package/dist/orchestrator/viewpoint-scaffold.d.ts.map +1 -0
- package/dist/orchestrator/viewpoint-scaffold.js +70 -0
- package/dist/orchestrator/viewpoint-scaffold.js.map +1 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +49 -10
- package/src/cli/commands/capability.ts +3 -1
- package/src/cli/commands/changed.ts +2 -4
- package/src/cli/commands/dashboard.ts +1 -26
- package/src/cli/commands/defect.ts +49 -0
- package/src/cli/commands/delivery.ts +70 -42
- package/src/cli/commands/results-merge.ts +7 -1
- package/src/cli/commands/select.ts +88 -0
- package/src/cli/commands/triage.ts +57 -0
- package/src/cli/commands/update.ts +3 -0
- package/src/cli/commands/viewpoint.ts +58 -0
- package/src/cli/index.ts +13 -2
- package/src/dashboard/templates/index.html +1 -1
- package/src/dashboard/types.ts +1 -0
- package/src/exporters/csv-exporter.ts +12 -11
- package/src/exporters/feature-parser.ts +12 -0
- package/src/exporters/json-exporter.ts +6 -1
- package/src/exporters/manual-hybrid-oracle.ts +112 -0
- package/src/exporters/matrix/build.ts +57 -11
- package/src/exporters/matrix/carry-manual-results.ts +70 -7
- package/src/exporters/matrix/export.ts +16 -2
- package/src/exporters/matrix/gates.ts +6 -0
- package/src/exporters/matrix/render-csv.ts +1 -1
- package/src/exporters/matrix/render-testcase-sheet.ts +10 -4
- package/src/exporters/matrix/render-xlsx.ts +3 -0
- package/src/exporters/matrix/types.ts +13 -1
- package/src/exporters/matrix/wording.ts +10 -1
- package/src/exporters/playwright-report-parser.ts +1 -0
- package/src/exporters/run-environment.ts +89 -0
- package/src/exporters/scenario-merger.ts +51 -5
- package/src/exporters/test-data-resolver.ts +0 -6
- package/src/exporters/types.ts +5 -0
- package/src/generators/gherkin-parser/index.ts +50 -18
- package/src/generators/test-generator/adapters/adapter-interface.ts +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-select-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/select-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/pick-option.hbs +13 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +16 -0
- package/src/generators/test-generator/code-generator.ts +23 -2
- package/src/generators/test-generator/diagnostics.ts +3 -1
- package/src/harness/audit.ts +52 -17
- package/src/harness/counted-assertions.ts +33 -0
- package/src/harness/data-driven-lint.ts +82 -24
- package/src/harness/defect-draft.ts +115 -0
- package/src/harness/defects.ts +94 -0
- package/src/harness/manifest.ts +2 -2
- package/src/harness/next-step.ts +47 -5
- package/src/harness/parse.ts +151 -27
- package/src/harness/quality-gates.ts +6 -4
- package/src/harness/read-text.ts +25 -0
- package/src/harness/script-check.ts +7 -1
- package/src/harness/select-by-ids.ts +83 -0
- package/src/harness/sensors.ts +91 -21
- package/src/harness/spec-branches.ts +1 -1
- package/src/harness/triage-signals.ts +46 -0
- package/src/harness/triage-unit.ts +113 -0
- package/src/harness/triage.ts +127 -0
- package/src/harness/ui-inventory.ts +164 -0
- package/src/harness/viewpoint-baseline.ts +7 -1
- package/src/harness/viewpoint-draft-header.ts +55 -0
- package/src/harness/viewpoint-ledger.ts +111 -24
- package/src/harness/viewpoint-mode.ts +49 -0
- package/src/harness/viewpoint-priority.ts +38 -0
- package/src/harness/viewpoint-review-open.ts +51 -0
- package/src/harness/viewpoint-scan-refs.ts +154 -0
- package/src/harness/viewpoint-scan.ts +111 -0
- package/src/orchestrator/flow-manager.ts +3 -53
- package/src/orchestrator/screen-manager.ts +7 -61
- package/src/orchestrator/templates/ai-src/agents/discovery.md +11 -3
- package/src/orchestrator/templates/ai-src/agents/reviewer.md +8 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +32 -24
- package/src/orchestrator/templates/ai-src/commands/review.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +78 -15
- package/src/orchestrator/templates/ai-src/skills/sungen-capture/mode-local.md +2 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +22 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +18 -9
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +5 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +58 -14
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +42 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +7 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +18 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +63 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +158 -0
- package/src/orchestrator/templates/playwright.config.ts +4 -2
- package/src/orchestrator/templates/specs-url-assert.ts +23 -2
- package/src/orchestrator/trace-mode-install.ts +94 -0
- package/src/orchestrator/viewpoint-pattern-catalog.ts +61 -0
- package/src/orchestrator/viewpoint-scaffold.ts +36 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defect records — `qa/defects/DEF-NNN-<slug>.md`, one project-wide folder. A scenario points at a
|
|
3
|
+
* record with `@known-defect:DEF-NNN`; the record points back at units, scenarios, claims and legacy
|
|
4
|
+
* TCs in its frontmatter. This module reads the records and checks both directions of that link.
|
|
5
|
+
* Drafting a record from a triage row lives in defect-draft.ts.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { parse as parseYaml } from 'yaml';
|
|
10
|
+
|
|
11
|
+
export const DEFECT_STATUSES = ['open', 'fixed', 'verified', 'closed', 'wont-fix'] as const;
|
|
12
|
+
export type DefectStatus = typeof DEFECT_STATUSES[number];
|
|
13
|
+
|
|
14
|
+
export interface DefectRecord {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
severity: string;
|
|
18
|
+
status: string;
|
|
19
|
+
units: string[];
|
|
20
|
+
scenarios: string[];
|
|
21
|
+
/** Project-relative path of the record file. */
|
|
22
|
+
file: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const DEF_FILE = /^(DEF-(\d+))-.*\.md$/;
|
|
26
|
+
const KNOWN_DEFECT_TAG = /@known-defect:(DEF-\d+)\b/g;
|
|
27
|
+
|
|
28
|
+
export function defectsDir(cwd: string): string {
|
|
29
|
+
return path.join(cwd, 'qa', 'defects');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function defectFiles(cwd: string): string[] {
|
|
33
|
+
const dir = defectsDir(cwd);
|
|
34
|
+
return fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => DEF_FILE.test(f)).sort() : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The next free id: one above the highest number on disk, zero-padded to 3. */
|
|
38
|
+
export function nextDefectId(cwd: string): string {
|
|
39
|
+
const max = defectFiles(cwd).reduce((m, f) => Math.max(m, Number(f.match(DEF_FILE)![2])), 0);
|
|
40
|
+
return `DEF-${String(max + 1).padStart(3, '0')}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const list = (v: unknown): string[] => (Array.isArray(v) ? v.map(String) : []);
|
|
44
|
+
|
|
45
|
+
/** Every record whose frontmatter parses; a malformed file is skipped, never thrown on. */
|
|
46
|
+
export function readDefects(cwd: string): DefectRecord[] {
|
|
47
|
+
const out: DefectRecord[] = [];
|
|
48
|
+
for (const f of defectFiles(cwd)) {
|
|
49
|
+
const file = path.join(defectsDir(cwd), f);
|
|
50
|
+
try {
|
|
51
|
+
const fm = parseYaml(fs.readFileSync(file, 'utf-8').split(/^---\s*$/m)[1] ?? '') as Record<string, unknown> | null;
|
|
52
|
+
if (!fm || typeof fm !== 'object') continue;
|
|
53
|
+
out.push({
|
|
54
|
+
id: String(fm.id ?? f.match(DEF_FILE)![1]),
|
|
55
|
+
title: String(fm.title ?? ''),
|
|
56
|
+
severity: String(fm.severity ?? ''),
|
|
57
|
+
status: String(fm.status ?? ''),
|
|
58
|
+
units: list(fm.units),
|
|
59
|
+
scenarios: list(fm.scenarios),
|
|
60
|
+
file: path.relative(cwd, file),
|
|
61
|
+
});
|
|
62
|
+
} catch {
|
|
63
|
+
// unreadable frontmatter: not a record we can reason about
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Advisory audit findings for one unit (never scored):
|
|
71
|
+
* DEFECT-REF-MISSING a `@known-defect:DEF-NNN` tag with no record file.
|
|
72
|
+
* DEFECT-ORPHAN (info) an OPEN record of this unit names a scenario the unit no longer has.
|
|
73
|
+
*/
|
|
74
|
+
export function defectLinkFindings(cwd: string, unitId: string, featureText: string, scenarioNames: string[]): string[] {
|
|
75
|
+
const records = readDefects(cwd);
|
|
76
|
+
const findings: string[] = [];
|
|
77
|
+
const known = new Set(records.map((r) => r.id));
|
|
78
|
+
const tagged = new Set([...featureText.matchAll(KNOWN_DEFECT_TAG)].map((m) => m[1]));
|
|
79
|
+
for (const id of tagged) {
|
|
80
|
+
if (!known.has(id)) {
|
|
81
|
+
findings.push(`DEFECT-REF-MISSING: \`@known-defect:${id}\` has no record in qa/defects/ — draft it with \`sungen defect draft --screen ${unitId}\`, or correct the id.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const names = new Set(scenarioNames);
|
|
85
|
+
for (const r of records) {
|
|
86
|
+
if (r.status !== 'open' || !r.units.includes(unitId)) continue;
|
|
87
|
+
for (const s of r.scenarios) {
|
|
88
|
+
if (!names.has(s)) {
|
|
89
|
+
findings.push(`DEFECT-ORPHAN (info): ${r.id} is open and names scenario "${s}", which ${unitId} no longer has — update the record's scenarios, or close it.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
package/src/harness/manifest.ts
CHANGED
|
@@ -13,6 +13,7 @@ import * as path from 'path';
|
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
14
|
import { featureFilesFor, reportSlug } from './unit-paths';
|
|
15
15
|
import { readTextFile } from './read-text';
|
|
16
|
+
import { vpCodeOf } from './parse';
|
|
16
17
|
|
|
17
18
|
export interface SpecSection { name: string; hash: string }
|
|
18
19
|
export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
|
|
@@ -90,8 +91,7 @@ function parseFeatureSections(featurePath: string): { scenario: string; vpCode?:
|
|
|
90
91
|
const s = line.match(/^Scenario:\s*(.*)$/);
|
|
91
92
|
if (s) {
|
|
92
93
|
const name = s[1].trim();
|
|
93
|
-
|
|
94
|
-
out.push({ scenario: name, vpCode: code, section });
|
|
94
|
+
out.push({ scenario: name, vpCode: vpCodeOf(name).vpCode, section });
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
return out;
|
package/src/harness/next-step.ts
CHANGED
|
@@ -16,9 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
19
|
+
import { parse as parseYaml } from 'yaml';
|
|
19
20
|
import { reportSlug, featureFilesFor } from './unit-paths';
|
|
20
21
|
import { readTextFile } from './read-text';
|
|
21
22
|
import { checkAssetsDrift } from '../orchestrator/assets-drift';
|
|
23
|
+
import { viewpointFileIsAiDraft } from './viewpoint-draft-header';
|
|
24
|
+
import { readViewpointDeclaration } from './viewpoint-baseline';
|
|
22
25
|
|
|
23
26
|
export interface NextStep {
|
|
24
27
|
/** The command to run, as the operator would type it. */
|
|
@@ -40,6 +43,8 @@ export interface UnitState {
|
|
|
40
43
|
kind: 'screen' | 'flow' | 'api';
|
|
41
44
|
hasSpec: boolean;
|
|
42
45
|
hasViewpoint: boolean;
|
|
46
|
+
/** test-viewpoint.md carries `drafted-by: ai` — written by the AI, not yet accepted by a QA. */
|
|
47
|
+
viewpointIsAiDraft?: boolean;
|
|
43
48
|
hasContract: boolean;
|
|
44
49
|
hasFlowInventory: boolean;
|
|
45
50
|
scenarioCount: number;
|
|
@@ -87,6 +92,29 @@ function countScenarios(unitDir: string, unit: string): number {
|
|
|
87
92
|
* Everything the resolver needs, read from disk. `unitId` is the catalog id
|
|
88
93
|
* (`<screen>` · `flows/<f>` · `api/<a>`); `unitDir` is its directory.
|
|
89
94
|
*/
|
|
95
|
+
const RESULT_FILE = /-test-result(?:\.[^.]+)*\.json$/;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Does the selector text resolve at least one element? A scaffold resolves none: the web one holds
|
|
99
|
+
* only the `page` route, the mobile one only header comments. Matching the WORD "placeholder" was
|
|
100
|
+
* wrong both ways (#868): it flagged a real file whose key names an input's placeholder text, and
|
|
101
|
+
* missed both scaffolds, which never contain it.
|
|
102
|
+
*/
|
|
103
|
+
function hasResolvedSelector(selectorText: string): boolean {
|
|
104
|
+
let doc: unknown;
|
|
105
|
+
try { doc = parseYaml(selectorText); } catch { return selectorText.trim().length >= 40; }
|
|
106
|
+
if (!doc || typeof doc !== 'object') return false;
|
|
107
|
+
// A real cell, not a marker left to be filled in. Numbers count (an id or index can be numeric).
|
|
108
|
+
const real = (v: unknown): boolean => (typeof v === 'string' || typeof v === 'number')
|
|
109
|
+
&& String(v).trim() !== '' && !/^(?:todo|tbd|placeholder|replace me)$/i.test(String(v).trim());
|
|
110
|
+
return Object.values(doc as Record<string, unknown>).some((e) => {
|
|
111
|
+
if (!e || typeof e !== 'object') return false;
|
|
112
|
+
const { type, value, name, locator } = e as { type?: unknown; value?: unknown; name?: unknown; locator?: unknown };
|
|
113
|
+
// Every SelectorEntry shape: a CSS `locator`, or a non-page `type` with a value or a role name.
|
|
114
|
+
return real(locator) || (typeof type === 'string' && type !== 'page' && (real(value) || real(name)));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
90
118
|
export function readUnitState(
|
|
91
119
|
projectRoot: string, unitDir: string, unit: string, kind: UnitState['kind'],
|
|
92
120
|
): UnitState {
|
|
@@ -101,7 +129,7 @@ export function readUnitState(
|
|
|
101
129
|
// `readTextFile` throws on a missing file, and every path here is legitimately absent at some
|
|
102
130
|
// point in a unit's life — the resolver exists to report exactly that, so it must not crash on it.
|
|
103
131
|
const read = (p: string): string => (fs.existsSync(p) ? readTextFile(p) : '');
|
|
104
|
-
const
|
|
132
|
+
const selectorTexts = selectorFiles.map((f) => read(path.join(selectorsDir, f)));
|
|
105
133
|
const contractPath = path.join(req, 'flow-contract.yaml');
|
|
106
134
|
const contractText = read(contractPath);
|
|
107
135
|
|
|
@@ -116,7 +144,9 @@ export function readUnitState(
|
|
|
116
144
|
});
|
|
117
145
|
const hasResults = specDirs.some((d) => {
|
|
118
146
|
try {
|
|
119
|
-
|
|
147
|
+
// `<f>-test-result[.<os>][.<env>].json` — a mobile run writes the OS into the name, an env
|
|
148
|
+
// run (SUNGEN_ENV=staging) the env; both are runs (#868).
|
|
149
|
+
return fs.readdirSync(d).some((f) => RESULT_FILE.test(f));
|
|
120
150
|
} catch { return false; }
|
|
121
151
|
});
|
|
122
152
|
let hasDeliverable = false;
|
|
@@ -131,6 +161,9 @@ export function readUnitState(
|
|
|
131
161
|
unit, kind,
|
|
132
162
|
hasSpec: exists(path.join(req, 'spec.md')),
|
|
133
163
|
hasViewpoint: exists(path.join(req, 'test-viewpoint.md')),
|
|
164
|
+
// An id-less draft has nothing to accept: only the missing-yardstick step speaks for it.
|
|
165
|
+
viewpointIsAiDraft: viewpointFileIsAiDraft(path.join(req, 'test-viewpoint.md'))
|
|
166
|
+
&& readViewpointDeclaration(path.join(req, 'test-viewpoint.md')) !== null,
|
|
134
167
|
hasContract: exists(contractPath),
|
|
135
168
|
// A contract WITHOUT `flows:` can only be scored per phase, which is a materially weaker
|
|
136
169
|
// claim — so it is part of the state, not a detail.
|
|
@@ -139,8 +172,7 @@ export function readUnitState(
|
|
|
139
172
|
hasSelectors: selectorFiles.length > 0,
|
|
140
173
|
// A scaffolded selector file exists but resolves nothing; treating it as "done" is what
|
|
141
174
|
// makes a run look further along than it is.
|
|
142
|
-
selectorsArePlaceholder: selectorFiles.length > 0
|
|
143
|
-
&& (/TODO|PLACEHOLDER|REPLACE ME/i.test(selectorText) || selectorText.trim().length < 40),
|
|
175
|
+
selectorsArePlaceholder: selectorFiles.length > 0 && !selectorTexts.some(hasResolvedSelector),
|
|
144
176
|
compiled, hasResults, hasDeliverable,
|
|
145
177
|
assetsDrift: (() => {
|
|
146
178
|
const d = checkAssetsDrift(projectRoot);
|
|
@@ -170,6 +202,7 @@ const REPAIRABLE = [
|
|
|
170
202
|
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'SPEC-CLAUSE-UNCOVERED',
|
|
171
203
|
'TRIGGER-UNCOVERED', 'PERMISSION-PAIR-MISSING', 'VISIBILITY-PAIR-MISSING',
|
|
172
204
|
'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
|
|
205
|
+
'UI-INVENTORY-MISSING', 'UI-INVENTORY-INCOMPLETE', 'UI-ITEM-UNCOVERED',
|
|
173
206
|
];
|
|
174
207
|
|
|
175
208
|
/**
|
|
@@ -219,6 +252,15 @@ export function deriveSteps(s: UnitState): NextStep[] {
|
|
|
219
252
|
});
|
|
220
253
|
return out;
|
|
221
254
|
}
|
|
255
|
+
// Before any generation: the AI drafts the viewpoint and audits it BEFORE it writes scenarios, so
|
|
256
|
+
// this is exactly where a draft would otherwise be accepted by the process that wrote it.
|
|
257
|
+
if (s.viewpointIsAiDraft) {
|
|
258
|
+
out.push({
|
|
259
|
+
kind: 'decide',
|
|
260
|
+
command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
|
|
261
|
+
because: 'test-viewpoint.md is marked `drafted-by: ai` — nobody has reviewed it, so the ledger and traceability axes are held as unverified. A QA reviews the claims (adds what matters, deletes what does not), then accepts; the command records the baseline and removes the marker. A decision, not a step: an agent presents it and does not run it',
|
|
262
|
+
});
|
|
263
|
+
}
|
|
222
264
|
if (s.scenarioCount === 0) {
|
|
223
265
|
out.push({ kind: 'do', command: design, because: 'no scenarios exist yet' });
|
|
224
266
|
return out;
|
|
@@ -234,7 +276,7 @@ export function deriveSteps(s: UnitState): NextStep[] {
|
|
|
234
276
|
out.push({ kind: 'do', command: `sungen audit --screen ${s.unit}`, because: 'the suite has never been measured' });
|
|
235
277
|
return out;
|
|
236
278
|
}
|
|
237
|
-
if (s.audit.viewpointBaselineStatus === 'changed') {
|
|
279
|
+
if (s.audit.viewpointBaselineStatus === 'changed' && !s.viewpointIsAiDraft) { // the draft's own decide step is already above
|
|
238
280
|
out.push({
|
|
239
281
|
kind: 'decide',
|
|
240
282
|
command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
|
package/src/harness/parse.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
|
-
import { readTextFile } from './read-text';
|
|
10
|
+
import { readTextFile, readViewpointText } from './read-text';
|
|
11
|
+
import { parsePriority, toEntryPriority } from './viewpoint-priority';
|
|
11
12
|
import { featureFilesFor } from './unit-paths';
|
|
12
13
|
|
|
13
14
|
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
@@ -15,15 +16,57 @@ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
|
15
16
|
export interface ViewpointEntry {
|
|
16
17
|
id: string; // e.g. VP-DATA-CONSISTENCY
|
|
17
18
|
priority: 'High' | 'Medium' | 'Low' | 'Unknown';
|
|
19
|
+
/** The declared priority word as written (`critical` · `high` · `medium` · `med` · `normal` · `low` ·
|
|
20
|
+
* `deferred` · `skip`, which also stands for skipped / n/a / none / not applicable), kept because `priority` folds several of them together and the
|
|
21
|
+
* PRIORITY-CONTRADICTED check needs them apart. Absent when no priority was declared. */
|
|
22
|
+
priorityWord?: string;
|
|
18
23
|
reason: string;
|
|
19
24
|
group?: 'Required' | 'Recommended' | 'Optional';
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Whole SECTIONS that declare something ABOUT the checklist rather than listing claims or
|
|
29
|
+
* viewpoints — known issues, spec gaps, traceability indexes, references and appendices, changelogs. Shared by the atomic
|
|
30
|
+
* ledger (their rows are not claims to prove, #630) and the viewpoint-overview parser (an id
|
|
31
|
+
* listed there is a reference, not a declaration, #833). `UI Patterns Identified` is the author's
|
|
32
|
+
* pick of WHICH sungen-viewpoint patterns apply — a `Skip` row there is not a claim to cover (#841).
|
|
33
|
+
* A `How to run` / `Procedure` section — that exact heading, optionally naming one id — is a manual
|
|
34
|
+
* check's steps, not claims of its own (#855); `How to test the login flow` stays a normal section.
|
|
35
|
+
*/
|
|
36
|
+
export const NON_DECLARATION_SECTION = /^(?:(?:ui patterns?(?: identified)?|(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?)(?:\s+(?:vp|fl)-[^\s:]+)?)\s*:?$|(?:known issues?|open questions?|spec gaps?|appendi(?:x|ces)|phụ lục|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Every word a viewpoint row may declare as its priority — ONE list for the overview parser, the
|
|
40
|
+
* atomic ledger and the integrity checks, which each carried their own copy and all missed the
|
|
41
|
+
* `Skip` that the `sungen add` / `add-flow` scaffolds themselves suggest (#853). `deferred` and the
|
|
42
|
+
* not-applicable words are declarations as well: such a row is not a claim to cover.
|
|
43
|
+
*/
|
|
44
|
+
export const PRIORITY_WORDS = 'critical|high|medium|med|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
|
|
45
|
+
/** "Declared not applicable" — folded to `skip` so every consumer checks one word. */
|
|
46
|
+
const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/i;
|
|
47
|
+
|
|
48
|
+
// The whole cell must BE the word (emphasis aside), as the old `| VP-… | high |` scan required:
|
|
49
|
+
// "Low risk" or "Not high" declares no priority.
|
|
50
|
+
const PRIORITY_WORD = new RegExp(`^[\\s*_\`]*(${PRIORITY_WORDS})[\\s*_\`.]*$`, 'i');
|
|
51
|
+
/** Header names of a priority column, in the languages viewpoint files are written in. */
|
|
52
|
+
const PRIORITY_HEADER = /^(?:priority|pri|level|severity|優先度?|độ ưu tiên|ưu tiên)(?:\s|\(|$)/i;
|
|
53
|
+
|
|
54
|
+
/** A priority cell → the folded level plus the word as written. */
|
|
55
|
+
function priorityOf(cell: string): { priority: ViewpointEntry['priority']; priorityWord?: string } {
|
|
56
|
+
const raw = cell.match(PRIORITY_WORD)?.[1].toLowerCase();
|
|
57
|
+
const word = raw && NOT_APPLICABLE.test(raw) ? 'skip' : raw;
|
|
58
|
+
const priority = toEntryPriority(parsePriority(cell));
|
|
59
|
+
return word ? { priority, priorityWord: word } : { priority };
|
|
60
|
+
}
|
|
61
|
+
|
|
22
62
|
export interface ScenarioInfo {
|
|
23
63
|
name: string;
|
|
24
64
|
vpCode?: string; // VP-CART-001 style code in the scenario title
|
|
25
65
|
category?: string; // CART, LIST, VAL, ... (from the code)
|
|
26
66
|
priority: Priority;
|
|
67
|
+
/** The priority tag as written, lower-cased, without `@` — keeps `critical` / `medium` / `med`, which
|
|
68
|
+
* `priority` has no slot for, so PRIORITY-CONTRADICTED can fold it like the declared word (#861). */
|
|
69
|
+
priorityTag?: string;
|
|
27
70
|
manual: boolean;
|
|
28
71
|
thenCount: number;
|
|
29
72
|
hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
|
|
@@ -39,8 +82,10 @@ export interface ScenarioInfo {
|
|
|
39
82
|
exploration?: boolean; // @exploration — a probe outside the official suite, exempt from source tracing
|
|
40
83
|
deferredToFlow?: boolean; // @deferred:flow — owned by a flow, not automated on this screen (H6)
|
|
41
84
|
ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
|
|
85
|
+
featureManual?: boolean; // its Feature is tagged @manual — codegen skips the whole file (#835)
|
|
42
86
|
/** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
|
|
43
|
-
|
|
87
|
+
/** `arg` = the step's data table, normalised (#817) — absent when the step has none. */
|
|
88
|
+
steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }>;
|
|
44
89
|
/**
|
|
45
90
|
* The `.feature` this scenario was read from. A unit may hold several (`add --feature`), and
|
|
46
91
|
* `scenarioIdReuse` scopes its uniqueness check to ONE file — so the sensor needs to know
|
|
@@ -106,7 +151,7 @@ function headingParts(line: string): { level: number; text: string } | null {
|
|
|
106
151
|
|
|
107
152
|
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
108
153
|
if (!fs.existsSync(filePath)) return [];
|
|
109
|
-
const text =
|
|
154
|
+
const text = readViewpointText(filePath);
|
|
110
155
|
const lines = text.split('\n');
|
|
111
156
|
|
|
112
157
|
const entries = new Map<string, ViewpointEntry>();
|
|
@@ -125,8 +170,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
125
170
|
// and an id read with its emphasis markers still attached matches no scenario tag.
|
|
126
171
|
const id = cells[0].replace(/[*`]/g, '').trim();
|
|
127
172
|
if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
|
|
128
|
-
|
|
129
|
-
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
173
|
+
entries.set(id.toUpperCase(), { id: id.toUpperCase(), ...priorityOf(cells[1]), reason: cells[2] });
|
|
130
174
|
}
|
|
131
175
|
}
|
|
132
176
|
}
|
|
@@ -179,6 +223,52 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
179
223
|
}
|
|
180
224
|
}
|
|
181
225
|
|
|
226
|
+
// 3) ID-first tables (#833): per-axis tables such as `| ID | Priority | Input | Expected |`.
|
|
227
|
+
// A row whose FIRST cell is a viewpoint id declares that id — the same rows the
|
|
228
|
+
// PRIORITY-CONTRADICTED check always read, so the two readers now agree. Kept narrow on purpose,
|
|
229
|
+
// because a viewpoint file also holds tables that only MENTION ids:
|
|
230
|
+
// - the id must be a `VP-`/`FL-` id, or belong to a family this file already declared above
|
|
231
|
+
// (`MS-HP` in Priority Viewpoints → `MS-HP-001`); `TC-01`, `FR-001`, `ISO-27001` never are;
|
|
232
|
+
// - reference sections (Known Issues, Traceability…) are skipped WITH their subsections, the
|
|
233
|
+
// way the ledger inherits the exclusion (#657), and fenced blocks are skipped;
|
|
234
|
+
// - the header is the row directly above a `|---|` separator (structure, not guessed words);
|
|
235
|
+
// its Priority column (any position, any alias) gives the priority, else column 2 does, as
|
|
236
|
+
// the old scan read it;
|
|
237
|
+
// - `reason` stays empty: viewpoint reasons feed page-type detection, and these rows were never
|
|
238
|
+
// entries before — the page type of an untouched file must not move.
|
|
239
|
+
const declaredFamilies = [...entries.keys()];
|
|
240
|
+
const familyOk = (id: string): boolean =>
|
|
241
|
+
/^(?:VP|FL)-/i.test(id) || declaredFamilies.some((f) => id.startsWith(`${f}-`) || id === f);
|
|
242
|
+
const isSeparator = (l: string): boolean => /^\|[\s|:-]+\|?$/.test(l) && l.includes('-');
|
|
243
|
+
let excludedAt: number | null = null;
|
|
244
|
+
let inFence = false;
|
|
245
|
+
let priorityCol = 1;
|
|
246
|
+
for (let i = 0; i < lines.length; i++) {
|
|
247
|
+
const line = lines[i].trim();
|
|
248
|
+
if (/^(```|~~~)/.test(line)) { inFence = !inFence; continue; }
|
|
249
|
+
if (inFence) continue;
|
|
250
|
+
const head = headingParts(line);
|
|
251
|
+
if (head) {
|
|
252
|
+
if (excludedAt !== null && head.level > excludedAt) continue; // subsection inherits
|
|
253
|
+
const text = head.text.replace(/[*`]/g, '').trim();
|
|
254
|
+
excludedAt = /^Priority Viewpoints/i.test(text) || NON_DECLARATION_SECTION.test(text) ? head.level : null;
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (excludedAt !== null || !line.startsWith('|') || isSeparator(line)) continue;
|
|
258
|
+
const cells = line.split('|').map((c) => c.trim()).filter((_, k, a) => k > 0 && k < a.length - 1);
|
|
259
|
+
if (isSeparator((lines[i + 1] ?? '').trim())) { // header row
|
|
260
|
+
const col = cells.findIndex((c) => PRIORITY_HEADER.test(c.replace(/[*`]/g, '').trim()));
|
|
261
|
+
priorityCol = col >= 0 ? col : 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const id = (cells[0] ?? '').replace(/[*`]/g, '').trim().toUpperCase();
|
|
265
|
+
if (!isViewpointId(id) || !familyOk(id)) continue;
|
|
266
|
+
const declared = priorityOf(cells[priorityCol] ?? '');
|
|
267
|
+
const existing = entries.get(id);
|
|
268
|
+
if (!existing) entries.set(id, { id, ...declared, reason: '' });
|
|
269
|
+
else if (!existing.priorityWord && declared.priorityWord) existing.priorityWord = declared.priorityWord;
|
|
270
|
+
}
|
|
271
|
+
|
|
182
272
|
return [...entries.values()];
|
|
183
273
|
}
|
|
184
274
|
|
|
@@ -199,6 +289,25 @@ function withBackground(sc: ParsedScenario, background?: ParsedScenario): Parsed
|
|
|
199
289
|
return { ...sc, steps: [...bg, ...((sc.steps as ParsedStep[]) || [])] } as ParsedScenario;
|
|
200
290
|
}
|
|
201
291
|
|
|
292
|
+
/**
|
|
293
|
+
* The `VP-<CATEGORY>-<n>` / `FL-<PHASE>-<n>` code in a scenario title, and its category — the ONE
|
|
294
|
+
* reading of it; the manifest kept an older copy of this regex and recorded no code for
|
|
295
|
+
* VP-I18N-001, VP-LIST-DISPLAY-01, VP-AF02-01 or any FL- id (#889).
|
|
296
|
+
*
|
|
297
|
+
* Category is everything between `VP-` and the final `-<sequence>` — INCLUDING hyphens, so
|
|
298
|
+
* compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
|
|
299
|
+
* not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
|
|
300
|
+
* Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
|
|
301
|
+
* flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
|
|
302
|
+
* Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
|
|
303
|
+
* VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
|
|
304
|
+
* VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
|
|
305
|
+
*/
|
|
306
|
+
export function vpCodeOf(title: string): { vpCode?: string; category?: string } {
|
|
307
|
+
const m = title.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
|
|
308
|
+
return m ? { vpCode: m[0].toUpperCase(), category: m[1].toUpperCase() } : {};
|
|
309
|
+
}
|
|
310
|
+
|
|
202
311
|
function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
203
312
|
const tags = sc.tags || [];
|
|
204
313
|
const deferredToFlow = tags.includes('@deferred:flow');
|
|
@@ -223,18 +332,11 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
223
332
|
const requiresCaps = tags.filter((t) => /^@requires:/i.test(t)).map((t) => t.slice('@requires:'.length).trim().toLowerCase()).filter(Boolean);
|
|
224
333
|
let priority: Priority = 'unknown';
|
|
225
334
|
for (const t of tags) if (PRIORITY_TAGS[t]) priority = PRIORITY_TAGS[t];
|
|
335
|
+
// Deliberately wider than `priority` (every priority word, any case): only PRIORITY-CONTRADICTED reads
|
|
336
|
+
// it, so `@critical` / `@High` are judged there without moving what select-by-ids or the score see.
|
|
337
|
+
const priorityTag = [...tags].reverse().find((t) => /^@(?:critical|high|medium|med|normal|low)$/i.test(t))?.slice(1).toLowerCase();
|
|
226
338
|
|
|
227
|
-
|
|
228
|
-
// compound categories (VP-LIST-DISPLAY-01, VP-ADD-TO-CART-03, VP-PRODUCT-DISCOVERY-02) parse,
|
|
229
|
-
// not just single-word ones. A single-word category (VP-CART-001) still works. (H1)
|
|
230
|
-
// Flows use journey-phase ids (FL-HP-001 / FL-ER-002) — the VP- anchor rejected them, so every
|
|
231
|
-
// flow scenario had NO category and the whole suite bucketed `other` (taxonomy=0%, #569).
|
|
232
|
-
// Segments may carry digits: the use-case framework numbers its flows (VP-AF02-01,
|
|
233
|
-
// VP-EF11-01 — one id per Alternate/Exception Flow), and `[A-Z]+` also silently truncated
|
|
234
|
-
// VP-I18N-001 to no category at all (the `18` broke the class mid-segment).
|
|
235
|
-
const codeMatch = sc.name.match(/\b(?:VP|FL)-([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-\d+/i);
|
|
236
|
-
const vpCode = codeMatch ? codeMatch[0].toUpperCase() : undefined;
|
|
237
|
-
const category = codeMatch ? codeMatch[1].toUpperCase() : undefined;
|
|
339
|
+
const { vpCode, category } = vpCodeOf(sc.name);
|
|
238
340
|
// Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
|
|
239
341
|
const leadMatch = sc.name.match(/^\s*([A-Za-z][A-Za-z0-9.-]*)/);
|
|
240
342
|
const vpId = leadMatch && isIdLike(leadMatch[1]) ? leadMatch[1].toUpperCase() : undefined;
|
|
@@ -246,22 +348,20 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
246
348
|
const skeletonParts: string[] = [];
|
|
247
349
|
const textParts: string[] = [sc.name];
|
|
248
350
|
const stepTextParts: string[] = [];
|
|
249
|
-
const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string }> = [];
|
|
351
|
+
const orderedSteps: Array<{ bucket: 'given' | 'when' | 'then'; text: string; arg?: string }> = [];
|
|
250
352
|
|
|
251
353
|
for (const step of sc.steps as ParsedStep[]) {
|
|
252
354
|
const kw = step.keyword.trim();
|
|
253
355
|
if (kw === 'Given' || kw === 'When' || kw === 'Then') last = kw;
|
|
254
356
|
textParts.push(step.text);
|
|
255
357
|
stepTextParts.push(step.text);
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
.trim()
|
|
264
|
-
.toLowerCase();
|
|
358
|
+
// The data table is part of what a step says (#817): `User see tab order:` with [FieldA]→[Cancel]
|
|
359
|
+
// and with [Save]→[Close] are different tests, and comparing step text alone called them exact
|
|
360
|
+
// duplicates and one oracle asserted N times. Kept OUT of `text`, which the claim/keyword sensors
|
|
361
|
+
// read as prose.
|
|
362
|
+
const arg = tableArg(step);
|
|
363
|
+
orderedSteps.push({ bucket: (kw === 'And' || kw === 'But' ? last : kw).toLowerCase() as 'given' | 'when' | 'then', text: step.text, ...(arg ? { arg } : {}) });
|
|
364
|
+
const skel = skeletonOf(step.text) + (arg ? ` ${skeletonOf(arg)}` : '');
|
|
265
365
|
skeletonParts.push(`${kw === 'And' || kw === 'But' ? last : kw}:${skel}`);
|
|
266
366
|
|
|
267
367
|
if (last === 'Then') {
|
|
@@ -286,6 +386,7 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
286
386
|
vpCode,
|
|
287
387
|
category,
|
|
288
388
|
priority,
|
|
389
|
+
priorityTag,
|
|
289
390
|
manual,
|
|
290
391
|
thenCount,
|
|
291
392
|
hasDataAssertion: hasData,
|
|
@@ -309,9 +410,11 @@ export function loadScenarios(featurePath: string): ScenarioInfo[] {
|
|
|
309
410
|
if (!fs.existsSync(featurePath)) return [];
|
|
310
411
|
const parser = new GherkinParser();
|
|
311
412
|
const feature = parser.parseFeatureFile(featurePath);
|
|
413
|
+
// Kept apart from `manual` so the audit's accounting is unchanged; only `select` reads it.
|
|
414
|
+
const featureManual = (feature.tags || []).some((t: string) => /^@manual\b/i.test(t)) || undefined;
|
|
312
415
|
return (feature.scenarios || [])
|
|
313
416
|
.filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
|
|
314
|
-
.map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath }));
|
|
417
|
+
.map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath, featureManual }));
|
|
315
418
|
}
|
|
316
419
|
|
|
317
420
|
/**
|
|
@@ -334,3 +437,24 @@ export function readUnitFeatureText(unitDir: string, unit: string): string {
|
|
|
334
437
|
.map((p) => readTextFile(p))
|
|
335
438
|
.join('\n');
|
|
336
439
|
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Normalised skeleton of step text: keep [refs] (distinct targets = distinct tests), but neutralise
|
|
443
|
+
* {{vars}} and quoted values so EP/data families collapse.
|
|
444
|
+
*/
|
|
445
|
+
function skeletonOf(text: string): string {
|
|
446
|
+
return text
|
|
447
|
+
.replace(/\{\{[^}]*\}\}/g, '{}')
|
|
448
|
+
.replace(/"[^"]*"/g, '""')
|
|
449
|
+
.replace(/\s+/g, ' ')
|
|
450
|
+
.trim()
|
|
451
|
+
.toLowerCase();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/** A step's data table as one comparable string — `<h1|h2 / c1|c2 / …>` — or undefined. */
|
|
455
|
+
function tableArg(step: ParsedStep): string | undefined {
|
|
456
|
+
const t = step.dataTable;
|
|
457
|
+
if (!t || (!t.headers.length && !t.rows.length)) return undefined;
|
|
458
|
+
const rows = [t.headers, ...t.rows.map((r) => r.cells)].map((cells) => cells.map((c) => c.trim()).join('|'));
|
|
459
|
+
return `<${rows.join(' / ')}>`;
|
|
460
|
+
}
|
|
@@ -8,6 +8,7 @@ import * as path from 'path';
|
|
|
8
8
|
import { keywordHit } from './keyword-match';
|
|
9
9
|
import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
|
|
10
10
|
import { parseManualComments } from '../exporters/scenario-merger';
|
|
11
|
+
import { commentLinesOf, isPresencePlaceholderHybrid, keywordStepsOf } from '../exporters/manual-hybrid-oracle';
|
|
11
12
|
import { readTextFile } from './read-text';
|
|
12
13
|
|
|
13
14
|
// ---------- #2 Downstream-scope ----------
|
|
@@ -220,14 +221,15 @@ export function manualOracle(featureText: string): ManualOracleResult {
|
|
|
220
221
|
// Observable:/Oracle:/Expected: line (parseManualComments — the SAME parser the
|
|
221
222
|
// delivery exporter uses, so this gate can't drift from the exported cells).
|
|
222
223
|
// Prose paragraphs / rationale-only comments export EMPTY Steps + Expected.
|
|
224
|
+
// • HAS a Then whose every assertion is a bare presence placeholder, with the real oracle
|
|
225
|
+
// in the comments (#810) — the same hybrid wearing a Then (manual-hybrid-oracle.ts, the
|
|
226
|
+
// SAME predicate as compile-time SG-W008).
|
|
223
227
|
const hasGherkinSteps = /^\s*(Given|When|Then)\s/m.test(b);
|
|
228
|
+
const commentLines = commentLinesOf(b);
|
|
224
229
|
let bad: boolean;
|
|
225
230
|
if (hasGherkinSteps) {
|
|
226
|
-
bad = !/^\s*Then\s/m.test(b);
|
|
231
|
+
bad = !/^\s*Then\s/m.test(b) || isPresencePlaceholderHybrid(keywordStepsOf(b), commentLines);
|
|
227
232
|
} else {
|
|
228
|
-
const commentLines = b.split('\n')
|
|
229
|
-
.filter((l) => /^\s*#/.test(l))
|
|
230
|
-
.map((l) => l.replace(/^\s*#\s?/, ''));
|
|
231
233
|
const parsed = parseManualComments(commentLines);
|
|
232
234
|
bad = !parsed.steps.length || !parsed.expected.length;
|
|
233
235
|
}
|
package/src/harness/read-text.ts
CHANGED
|
@@ -32,3 +32,28 @@ export function readTextFile(filePath: string): string {
|
|
|
32
32
|
export function readTextLines(filePath: string): string[] {
|
|
33
33
|
return readTextFile(filePath).split('\n');
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drop HTML comments (`<!-- … -->`, multi-line too) — they are author notes and scaffold samples,
|
|
38
|
+
* never content. Newlines inside a comment are kept so every later line keeps its position (#841).
|
|
39
|
+
*
|
|
40
|
+
* Only a CLOSED comment is dropped. An unclosed `<!--` (a forgotten `-->`) stays text: dropping it
|
|
41
|
+
* to the end of the file would silently delete every declaration below it. A fenced code block is
|
|
42
|
+
* content, and so is a `<!--` written inside backticks: both are how a document SHOWS the syntax.
|
|
43
|
+
*/
|
|
44
|
+
export function stripHtmlComments(text: string): string {
|
|
45
|
+
// A comment body never holds another `<!--`: an opener with no `-->` before the next opener is
|
|
46
|
+
// the unclosed one, and must not reach a LATER comment's closer and eat everything between.
|
|
47
|
+
const blank = (s: string): string => s.replace(/(?<!`)<!--(?:(?!<!--)[\s\S])*?-->/g, (m) => m.replace(/[^\n]/g, ''));
|
|
48
|
+
// split() with a capture group keeps the fences at the odd indexes.
|
|
49
|
+
return text.split(/(^```[^\n]*\n[\s\S]*?^```[^\n]*$)/m).map((part, i) => (i % 2 ? part : blank(part))).join('');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `test-viewpoint.md` as every harness sensor must read it: normalised, comments removed, and
|
|
54
|
+
* '' when the unit has none. `sungen add` scaffolds its samples inside `<!-- -->`; read raw, an
|
|
55
|
+
* untouched scaffold declared VP-UI/VP-VAL/VP-LOGIC/VP-SEC and carried 12 phantom claims (#841).
|
|
56
|
+
*/
|
|
57
|
+
export function readViewpointText(filePath: string): string {
|
|
58
|
+
return fs.existsSync(filePath) ? stripHtmlComments(readTextFile(filePath)) : '';
|
|
59
|
+
}
|
|
@@ -107,6 +107,12 @@ const ASSERTION_STEP_COMMENT = /^\s*\/\/\s*Assert\b/;
|
|
|
107
107
|
// but as a bare substring it would also exempt a genuinely hollow step whose test-data or step
|
|
108
108
|
// text happened to contain it.
|
|
109
109
|
const NOOP_STEP_MARKER = /^\s*\/\/\s*App launched via wdio capabilities/;
|
|
110
|
+
// The same kind of step on the web adapter, where the explanation IS the step comment: the
|
|
111
|
+
// step-mapper compiles `Given User is on [Edit] dialog` (and its exit) to `// Enter dialog scope:
|
|
112
|
+
// Edit` and no code, because it only re-roots the steps after it — the next assertion runs as
|
|
113
|
+
// `page.getByRole('dialog', …).getByRole(…)`. A field unit reported 13 BYPASS, every one of them
|
|
114
|
+
// this comment, and exited 2 on a correct suite (#881). Anchored to the whole marker line.
|
|
115
|
+
const SCOPE_STEP_MARKER = /^\s*\/\/\s*(?:Enter|Exit) dialog scope(?::\s*.*)?$/;
|
|
110
116
|
|
|
111
117
|
/** A spec comment line reduced to its text, for comparison against a compiler step comment. */
|
|
112
118
|
const commentText = (line: string): string => line.trim().replace(/^\/\/\s?/, '').trim();
|
|
@@ -241,7 +247,7 @@ export function analyzeFaithfulness(
|
|
|
241
247
|
const hasCode = region.some((l) => !isComment(l) && CODE_LINE.test(l));
|
|
242
248
|
// A documented no-op emits its explanation INTO the step's region, so read the region
|
|
243
249
|
// (not just the marker line) for it — see NOOP_STEP_MARKER.
|
|
244
|
-
if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l))) continue;
|
|
250
|
+
if (hasCode || region.some((l) => NOOP_STEP_MARKER.test(l)) || SCOPE_STEP_MARKER.test(body[markers[s]])) continue;
|
|
245
251
|
if (tailAmbiguous && s === markers.length - 1) continue;
|
|
246
252
|
hollowSteps.push({ test: title, step: body[markers[s]].trim().slice(0, 70) });
|
|
247
253
|
}
|