@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,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #811 — the design inventory: is every item the DESIGN shows accounted for by the suite?
|
|
3
|
+
*
|
|
4
|
+
* Design sources in `requirements/ui/` (images, `figma-meta.md`) and `requirements/spec_figma.md`
|
|
5
|
+
* were read "for visual context" and never turned into a checkable list, so a column that exists
|
|
6
|
+
* only on the design — or contradicts the spec — was missed by the author AND the reviewer, and a
|
|
7
|
+
* deliberate deferral looked exactly like an oversight.
|
|
8
|
+
*
|
|
9
|
+
* create-test now writes `requirements/ui-inventory.md`: one `UI-NNN` row per component / column /
|
|
10
|
+
* button / state / annotation, with its source (`spec+design` | `design-only` | `conflict`). An
|
|
11
|
+
* item is accounted for by a scenario tag (`@ui:UI-014`) or a reasoned deferral comment
|
|
12
|
+
* (`# UI-DEFERRED: UI-002..013 — <reason>`) — no other mention counts; ranges and `/`
|
|
13
|
+
* enumerations read the same way as spec ids (spec-coverage `citedIds`). `spec+design` items are already measured
|
|
14
|
+
* by spec-coverage, so only `design-only` / `conflict` items are reported uncovered.
|
|
15
|
+
*
|
|
16
|
+
* Advisory: findings never move the score or the gate.
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import { citedIds } from './spec-coverage';
|
|
21
|
+
import { readTextFile } from './read-text';
|
|
22
|
+
|
|
23
|
+
export type UiItemSource = 'spec+design' | 'design-only' | 'conflict';
|
|
24
|
+
|
|
25
|
+
export interface UiItem {
|
|
26
|
+
id: string;
|
|
27
|
+
item: string;
|
|
28
|
+
kind: string;
|
|
29
|
+
source: UiItemSource;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UiInventoryResult {
|
|
33
|
+
/** Design sources the unit holds: image files, figma-meta.md, spec_figma.md. */
|
|
34
|
+
designSources: string[];
|
|
35
|
+
/** `requirements/ui-inventory.md` exists. */
|
|
36
|
+
hasInventory: boolean;
|
|
37
|
+
items: UiItem[];
|
|
38
|
+
/** `images read: a/b` from the inventory header — null when the line is absent. */
|
|
39
|
+
imagesRead: { read: number; total: number } | null;
|
|
40
|
+
/** Images on disk, for comparing against `imagesRead.total`. */
|
|
41
|
+
imagesOnDisk: number;
|
|
42
|
+
covered: string[];
|
|
43
|
+
deferred: string[];
|
|
44
|
+
/** design-only / conflict items neither covered nor deferred. */
|
|
45
|
+
uncovered: UiItem[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const IMAGE = /\.(png|jpe?g|webp|gif)$/i;
|
|
49
|
+
|
|
50
|
+
/** Every design source in a unit's `requirements/` — what makes an inventory necessary. */
|
|
51
|
+
export function designSourcesOf(reqDir: string): string[] {
|
|
52
|
+
const out: string[] = [];
|
|
53
|
+
const uiDir = path.join(reqDir, 'ui');
|
|
54
|
+
try {
|
|
55
|
+
for (const f of fs.readdirSync(uiDir).sort()) {
|
|
56
|
+
if (IMAGE.test(f) || f === 'figma-meta.md') out.push(`ui/${f}`);
|
|
57
|
+
}
|
|
58
|
+
} catch { /* no ui/ dir */ }
|
|
59
|
+
if (fs.existsSync(path.join(reqDir, 'spec_figma.md'))) out.push('spec_figma.md');
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The Source cell, read leniently ("design only", "Spec + Design", "conflict — …"). Anything else
|
|
65
|
+
* is NOT assumed to be spec-backed: an unreadable source is reported like a design-only item, so a
|
|
66
|
+
* typo can never make an item disappear from the check.
|
|
67
|
+
*/
|
|
68
|
+
function sourceOf(cell: string): UiItemSource | null {
|
|
69
|
+
const c = cell.trim().toLowerCase();
|
|
70
|
+
if (/^design[\s_-]*only\b/.test(c)) return 'design-only';
|
|
71
|
+
if (/^conflict\b/.test(c)) return 'conflict';
|
|
72
|
+
if (/^spec\s*(?:\+|&|and)\s*design\b/.test(c)) return 'spec+design';
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parse the inventory table: any markdown row whose first cell is a `UI-<n>` id. Leading/trailing
|
|
78
|
+
* pipes are optional (GFM), and the Source cell is FOUND rather than counted — a `|` written inside
|
|
79
|
+
* the Item cell shifts the columns, and a positional read then misfiled a design-only row.
|
|
80
|
+
*/
|
|
81
|
+
export function parseUiInventory(text: string): { items: UiItem[]; imagesRead: UiInventoryResult['imagesRead'] } {
|
|
82
|
+
const items: UiItem[] = [];
|
|
83
|
+
for (const line of text.split('\n')) {
|
|
84
|
+
if (!line.includes('|')) continue;
|
|
85
|
+
const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
|
|
86
|
+
if (!/^UI-\d+$/i.test(cells[0] ?? '')) continue;
|
|
87
|
+
// Source = the last cell that reads as one (Seen-in follows it; prose in Item never starts so).
|
|
88
|
+
let srcIdx = -1;
|
|
89
|
+
for (let i = cells.length - 1; i >= 2; i--) if (sourceOf(cells[i])) { srcIdx = i; break; }
|
|
90
|
+
const source: UiItemSource = srcIdx >= 0 ? sourceOf(cells[srcIdx])! : 'design-only';
|
|
91
|
+
const kindIdx = srcIdx >= 0 ? srcIdx - 1 : Math.min(2, cells.length - 1);
|
|
92
|
+
items.push({
|
|
93
|
+
id: cells[0].toUpperCase(),
|
|
94
|
+
item: cells.slice(1, Math.max(2, kindIdx)).join(' | '),
|
|
95
|
+
kind: cells[kindIdx] ?? '',
|
|
96
|
+
source,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const m = text.match(/images read:\s*(\d+)\s*\/\s*(\d+)/i);
|
|
100
|
+
return { items, imagesRead: m ? { read: Number(m[1]), total: Number(m[2]) } : null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Ids cited on a `UI-DEFERRED` comment line — a deliberate, REASONED omission. A line that lists
|
|
105
|
+
* ids and nothing else is not a decision anyone can review, so it defers nothing.
|
|
106
|
+
*/
|
|
107
|
+
function deferredIds(featureText: string): Set<string> {
|
|
108
|
+
const lines = featureText.split('\n').filter((l) => {
|
|
109
|
+
if (!/^\s*#\s*UI-DEFERRED\b/i.test(l)) return false;
|
|
110
|
+
const reason = l.replace(/^\s*#\s*UI-DEFERRED\b\s*:?/i, '').replace(/\bUI-\d+(?:\s*(?:\.\.|–|—|~|\/)\s*(?:UI-)?\d+)*/gi, '');
|
|
111
|
+
return /[\p{L}]{3,}/u.test(reason);
|
|
112
|
+
});
|
|
113
|
+
return citedIds(lines.join('\n'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function uiInventoryCoverage(reqDir: string, featureText: string): UiInventoryResult {
|
|
117
|
+
const designSources = designSourcesOf(reqDir);
|
|
118
|
+
const invPath = path.join(reqDir, 'ui-inventory.md');
|
|
119
|
+
const invText = fs.existsSync(invPath) ? readTextFile(invPath) : null;
|
|
120
|
+
const { items, imagesRead } = invText ? parseUiInventory(invText) : { items: [], imagesRead: null };
|
|
121
|
+
// Coverage is the `@ui:` TAG, nothing else: a `UI-001`-shaped string elsewhere (an `@spec:UI-001`
|
|
122
|
+
// requirement in a project that numbers its FRs that way, prose, an unreasoned deferral line)
|
|
123
|
+
// must not mark a design item as proven.
|
|
124
|
+
const cites = citedIds((featureText.match(/@ui:\S+/gi) ?? []).join(' '));
|
|
125
|
+
const deferSet = deferredIds(featureText);
|
|
126
|
+
const covered: string[] = [];
|
|
127
|
+
const deferred: string[] = [];
|
|
128
|
+
const uncovered: UiItem[] = [];
|
|
129
|
+
for (const it of items) {
|
|
130
|
+
if (deferSet.has(it.id)) deferred.push(it.id);
|
|
131
|
+
else if (cites.has(it.id)) covered.push(it.id);
|
|
132
|
+
else if (it.source !== 'spec+design') uncovered.push(it);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
designSources,
|
|
136
|
+
hasInventory: invText !== null,
|
|
137
|
+
items,
|
|
138
|
+
imagesRead,
|
|
139
|
+
imagesOnDisk: designSources.filter((s) => IMAGE.test(s)).length,
|
|
140
|
+
covered,
|
|
141
|
+
deferred,
|
|
142
|
+
uncovered,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Advisory findings for the audit report. */
|
|
147
|
+
export function uiInventoryFindings(r: UiInventoryResult): string[] {
|
|
148
|
+
const out: string[] = [];
|
|
149
|
+
if (r.designSources.length === 0) return out;
|
|
150
|
+
if (!r.hasInventory) {
|
|
151
|
+
out.push(`UI-INVENTORY-MISSING: the unit has design sources (${r.designSources.slice(0, 4).join(', ')}${r.designSources.length > 4 ? ', …' : ''}) but no \`requirements/ui-inventory.md\` — nothing shows the design was read item by item, so an item that exists only on the design (or contradicts the spec) is invisible to the gate and to review. Re-run /sungen:create-test so it writes the inventory, or write it by hand: one \`| UI-NNN | item | kind | spec+design / design-only / conflict |\` row per component, column, button, state and annotation.`);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
if (r.imagesRead && r.imagesRead.read < Math.max(r.imagesRead.total, r.imagesOnDisk)) {
|
|
155
|
+
out.push(`UI-INVENTORY-INCOMPLETE: the inventory records "images read: ${r.imagesRead.read}/${r.imagesRead.total}" and the unit holds ${r.imagesOnDisk} image(s) — the unread design is not inventoried. Read the remaining images and add their items.`);
|
|
156
|
+
} else if (!r.imagesRead && r.imagesOnDisk > 0) {
|
|
157
|
+
out.push(`UI-INVENTORY-INCOMPLETE: the unit holds ${r.imagesOnDisk} image(s) but the inventory does not record "images read: N/N" — nothing shows every image was opened. Add the line after reading them all.`);
|
|
158
|
+
}
|
|
159
|
+
for (const it of r.uncovered.slice(0, 10)) {
|
|
160
|
+
out.push(`UI-ITEM-UNCOVERED: ${it.id} "${it.item}" (${it.kind}, ${it.source}) has no scenario and no deferral — ${it.source === 'conflict' ? 'the design and spec.md disagree on it; resolve which is right, then cover it' : 'it exists only on the design, outside spec.md'}. Cover it (\`@ui:${it.id}\` on the scenario that proves it), or record the decision: \`# UI-DEFERRED: ${it.id} — <reason>\`.`);
|
|
161
|
+
}
|
|
162
|
+
if (r.uncovered.length > 10) out.push(`UI-ITEM-UNCOVERED: … and ${r.uncovered.length - 10} more design-only/conflict item(s).`);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { parseViewpointOverview } from './parse';
|
|
5
|
+
import { isAiDraft, removeDraftLine } from './viewpoint-draft-header';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Keep the yardstick from moving with the thing it measures.
|
|
@@ -89,7 +90,12 @@ function fingerprint(ids: string[]): string {
|
|
|
89
90
|
*/
|
|
90
91
|
function fileFingerprint(viewpointPath: string): string {
|
|
91
92
|
try {
|
|
92
|
-
|
|
93
|
+
const bytes = fs.readFileSync(viewpointPath);
|
|
94
|
+
// `drafted-by: ai` is provenance that accept REMOVES, so it is not part of what was reviewed:
|
|
95
|
+
// counted, accepting a draft would read as the declaration changing. Every other byte stays raw.
|
|
96
|
+
const text = bytes.toString('utf-8');
|
|
97
|
+
const hashed = isAiDraft(text) ? Buffer.from(removeDraftLine(text), 'utf-8') : bytes;
|
|
98
|
+
return crypto.createHash('sha1').update(hashed).digest('hex').slice(0, 12);
|
|
93
99
|
} catch { return ''; }
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { stripHtmlComments } from './read-text';
|
|
3
|
+
import { writeFileAtomic } from './write-atomic';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `drafted-by: ai` — provenance, not acceptance. The AI writes it as the first header line of a
|
|
7
|
+
* viewpoint it drafted; while it is there the file is an unreviewed draft: the audit records no
|
|
8
|
+
* baseline for it and holds the two axes measured against it. `--accept-viewpoint` removes it.
|
|
9
|
+
*
|
|
10
|
+
* Read by a line scan, not by the viewpoint parser: the key must be found in a file the parser
|
|
11
|
+
* judges nothing about, and in a QA-written file it simply is not there.
|
|
12
|
+
*/
|
|
13
|
+
const DRAFT_KEY = /^\s*drafted-by:\s*ai\b/i;
|
|
14
|
+
/** The key is a header line: looking further down would read prose that merely mentions it. */
|
|
15
|
+
const HEADER_LINES = 15;
|
|
16
|
+
|
|
17
|
+
/** A line with its own terminator, so a removal keeps CRLF and a lone `\r` exactly as written. */
|
|
18
|
+
const withTerminator = (raw: string): string[] => raw.match(/[^\r\n]*(?:\r\n|\r|\n)|[^\r\n]+$/g) ?? [];
|
|
19
|
+
|
|
20
|
+
/** Index of the draft line in `raw`, or -1. Comments are blanked line-for-line, so indexes agree with the raw file. */
|
|
21
|
+
function draftLineIndex(raw: string): number {
|
|
22
|
+
const visible = stripHtmlComments(raw.replace(/\r\n?/g, '\n')).split('\n');
|
|
23
|
+
let seen = 0;
|
|
24
|
+
for (let i = 0; i < visible.length && seen < HEADER_LINES; i++) {
|
|
25
|
+
if (visible[i].trim() === '') continue;
|
|
26
|
+
if (DRAFT_KEY.test(visible[i])) return i;
|
|
27
|
+
seen++;
|
|
28
|
+
}
|
|
29
|
+
return -1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isAiDraft(text: string): boolean {
|
|
33
|
+
return draftLineIndex(text) >= 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `raw` without the one draft line; every other byte is untouched. A file with no draft line comes back as is. */
|
|
37
|
+
export function removeDraftLine(raw: string): string {
|
|
38
|
+
const at = draftLineIndex(raw);
|
|
39
|
+
if (at < 0) return raw;
|
|
40
|
+
return withTerminator(raw).filter((_, i) => i !== at).join('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function viewpointFileIsAiDraft(viewpointPath: string): boolean {
|
|
44
|
+
try { return isAiDraft(fs.readFileSync(viewpointPath, 'utf-8')); } catch { return false; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Remove the draft line from the file, atomically. True when a line was removed; throws a clear error when the write fails. */
|
|
48
|
+
export function removeDraftLineFromFile(viewpointPath: string): boolean {
|
|
49
|
+
const raw = fs.readFileSync(viewpointPath, 'utf-8');
|
|
50
|
+
if (!isAiDraft(raw)) return false;
|
|
51
|
+
try { writeFileAtomic(viewpointPath, removeDraftLine(raw)); } catch (err) {
|
|
52
|
+
throw new Error(`could not remove the \`drafted-by: ai\` line from ${viewpointPath} (${err instanceof Error ? err.message : err}) — the file is unchanged; remove the line by hand`);
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* gaps that inflate a "looks-covered" score; it does not fail the gate.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
-
import { ScenarioInfo } from './parse';
|
|
13
|
-
import {
|
|
12
|
+
import { NON_DECLARATION_SECTION, PRIORITY_WORDS, ScenarioInfo } from './parse';
|
|
13
|
+
import { readViewpointText } from './read-text';
|
|
14
14
|
import { keywordHit } from './keyword-match';
|
|
15
|
+
import { countedAssertions } from './counted-assertions';
|
|
15
16
|
|
|
16
17
|
export interface LedgerItem { id?: string; text: string; covered: boolean }
|
|
17
18
|
|
|
@@ -57,6 +58,13 @@ function claimedCount(text: string): number | null {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.Title, VP7-002, MS-HP-001, TV-01
|
|
61
|
+
/**
|
|
62
|
+
* A bullet declares only the id it STARTS with (after a `[ ]` checkbox); one further along is a
|
|
63
|
+
* citation (`… (SG-01, …)`). `- **High** VP-…` or `- Email shown — VP-…` therefore carry no id.
|
|
64
|
+
*/
|
|
65
|
+
const BULLET_ID = new RegExp(`^[\\s(#]*(?:\\[[ xX]\\]\\s*)?\\[?${ID_RE.source}`);
|
|
66
|
+
/** A table cell that is an id and nothing else. */
|
|
67
|
+
const WHOLE_ID = new RegExp(`^${ID_RE.source}$`);
|
|
60
68
|
|
|
61
69
|
/**
|
|
62
70
|
* Lines that DECLARE something about the checklist rather than being a checklist item —
|
|
@@ -69,7 +77,10 @@ const ID_RE = /\b([A-Z]{1,5}\d{0,2}(?:[.\-][A-Za-z0-9]+)*-?\d{0,3})\b/; // VP0.T
|
|
|
69
77
|
const PLACEHOLDER_ITEM = /^(none\b|n\/a\b|tbd\b|no known\b|nothing\b|do not invent\b|-+$)/i;
|
|
70
78
|
/** A priority-DECLARATION row: `| VP-LOGIC | High | <reason prose> |`. The category id and its
|
|
71
79
|
* priority are consumed by the traceability + balance axes; the reason is rationale. */
|
|
72
|
-
|
|
80
|
+
// The word must be the WHOLE second cell: `skip` / `none` / `n/a` also open real claims
|
|
81
|
+
// ("Skip link moves focus to main", "None of the optional fields block submit") — and so did
|
|
82
|
+
// `high`/`low` ("High contrast mode …"), which the old word-boundary match swallowed too.
|
|
83
|
+
const PRIORITY_ROW = new RegExp(`^(?:VP|FL)[A-Z0-9._-]*\\s+—\\s+[*_\`]*(?:${PRIORITY_WORDS})[*_\`.]*(?=\\s+—|\\s*$)`, 'i');
|
|
73
84
|
/**
|
|
74
85
|
* Whole SECTIONS that declare something ABOUT the checklist rather than listing claims to prove.
|
|
75
86
|
*
|
|
@@ -82,49 +93,121 @@ const PRIORITY_ROW = /^(?:VP|FL)[A-Z0-9._-]*\s+—\s+(?:critical|high|medium|nor
|
|
|
82
93
|
* `Design Decisions` deliberately stays IN: "values are buffered between step 3 and 4, a reload
|
|
83
94
|
* loses them" is a real claim, and it is what the continuity check reads.
|
|
84
95
|
*/
|
|
85
|
-
const DECLARATION_SECTION =
|
|
96
|
+
const DECLARATION_SECTION = NON_DECLARATION_SECTION; // shared with the overview parser (#833)
|
|
97
|
+
/**
|
|
98
|
+
* A lead-in that introduces a manual check's PROCEDURE — `How to run VP-SEC-1:`, `Procedures:`,
|
|
99
|
+
* `Steps to reproduce:`. The list under it is how to EXECUTE a claim already declared above;
|
|
100
|
+
* counting each step as its own claim made gaps no scenario can close (8 of 13 surplus items on a
|
|
101
|
+
* field unit, #855).
|
|
102
|
+
*
|
|
103
|
+
* Deliberately narrow, because a false match HIDES claims: the lead may name one id and nothing
|
|
104
|
+
* else (`How to verify the following claims hold:` introduces claims, not steps), and a bare
|
|
105
|
+
* `Steps:` (a flow lists real step claims under it) and `Tester verifies:` (expected outcomes —
|
|
106
|
+
* exactly what the ledger counts) never qualify.
|
|
107
|
+
*/
|
|
108
|
+
const PROCEDURE_LEAD = /^(?:how to (?:run|test|reproduce|verify)|(?:manual )?procedures?|manual steps|steps to reproduce)(?:\s+(?:VP|FL)-[\w.-]+)?\s*:$/i;
|
|
109
|
+
const LIST_ITEM = /^(?:[-*+]|\d+[.)])\s+/;
|
|
86
110
|
const GENERIC = new Set(['display', 'shown', 'value', 'field', 'input', 'page', 'screen', 'button', 'link', 'text', 'check', 'verify', 'should', 'with', 'when', 'then', 'user', 'this', 'that', 'each', 'item', 'items']);
|
|
87
111
|
|
|
112
|
+
/**
|
|
113
|
+
* `decision`: a `- D4: …` line under Design Decisions. It names the spec's intent so rows can cite
|
|
114
|
+
* it; its testable part is carried by a VP row (review check 8). No scenario ever carries `D4`, so
|
|
115
|
+
* as a ledger item it stayed MISSING for good — "not tested" decisions included (phase-7 D5). The
|
|
116
|
+
* ledger skips it; the continuity, source and integrity checks still read it. Only that section
|
|
117
|
+
* declares: a QA claim labelled `D8` elsewhere stays a claim, and an id-less decision bullet stays
|
|
118
|
+
* the prose claim it always was.
|
|
119
|
+
*/
|
|
120
|
+
export interface ViewpointItem { id?: string; text: string; decision?: true }
|
|
121
|
+
const DECISION_ID = /^D\d+$/;
|
|
122
|
+
const DECISIONS_SECTION = /^(?:design decisions?|quyết định(?: thiết kế)?)\b/i;
|
|
123
|
+
|
|
88
124
|
/** Extract atomic checklist items from a viewpoint file (format-tolerant). */
|
|
89
|
-
export function parseViewpointItems(viewpointPath: string):
|
|
125
|
+
export function parseViewpointItems(viewpointPath: string): ViewpointItem[] {
|
|
90
126
|
if (!fs.existsSync(viewpointPath)) return [];
|
|
91
|
-
const lines =
|
|
92
|
-
const items:
|
|
127
|
+
const lines = readViewpointText(viewpointPath).split('\n');
|
|
128
|
+
const items: ViewpointItem[] = [];
|
|
93
129
|
let inFence = false;
|
|
94
130
|
let inDeclarationSection = false;
|
|
95
131
|
// The level an excluded section was opened at. A SUBSECTION inherits it: `### Optional`
|
|
96
132
|
// nested under `## 8. Traceability Index` used to re-open the section and hand its rows
|
|
97
133
|
// back as claims, because exclusion was recomputed from scratch at every heading.
|
|
98
134
|
let excludedAt: number | null = null;
|
|
99
|
-
|
|
100
|
-
|
|
135
|
+
// The level the Design Decisions section was opened at, while inside it (see `decision`).
|
|
136
|
+
let decisionsAt: number | null = null;
|
|
137
|
+
// Inside the list a PROCEDURE_LEAD introduced: the indent of the lead (-1 for a paragraph lead),
|
|
138
|
+
// whose list items — and their wrapped continuation lines — are steps. A heading, a table, a
|
|
139
|
+
// new lead, or (after a paragraph lead) a blank line once a step was read ends it: when in
|
|
140
|
+
// doubt a step stays a claim, because a swallowed claim is a gap nobody ever sees — so a LOOSE
|
|
141
|
+
// procedure (steps separated by blank lines) leaks its later steps as claims, by design.
|
|
142
|
+
// Only an INDENTED line continues a step (Markdown's continuation); unindented prose ends it.
|
|
143
|
+
let procIndent: number | null = null;
|
|
144
|
+
let procSteps = 0;
|
|
145
|
+
let prevBlank = false;
|
|
146
|
+
for (let i = 0; i < lines.length; i++) {
|
|
147
|
+
const line = lines[i].trim();
|
|
101
148
|
if (line.startsWith('```')) { inFence = !inFence; continue; }
|
|
102
|
-
if (inFence
|
|
149
|
+
if (inFence) continue;
|
|
150
|
+
if (!line) { prevBlank = true; continue; }
|
|
151
|
+
const wasBlank = prevBlank;
|
|
152
|
+
prevBlank = false;
|
|
153
|
+
const isItem = LIST_ITEM.test(line);
|
|
154
|
+
const indent = lines[i].length - lines[i].trimStart().length;
|
|
155
|
+
if (PROCEDURE_LEAD.test(line.replace(LIST_ITEM, '').replace(/[*_`]/g, '').trim())) {
|
|
156
|
+
procIndent = isItem ? indent : -1;
|
|
157
|
+
procSteps = 0;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (procIndent !== null) {
|
|
161
|
+
const listEnded = wasBlank && procIndent < 0 && procSteps > 0;
|
|
162
|
+
if (!listEnded && isItem && indent > procIndent) { procSteps++; continue; } // a step
|
|
163
|
+
if (!listEnded && !isItem && !wasBlank && procSteps > 0 && indent > Math.max(procIndent, 0)
|
|
164
|
+
&& !/^[#|]/.test(line) && !line.endsWith(':')) continue; // a step that wraps
|
|
165
|
+
procIndent = null;
|
|
166
|
+
}
|
|
103
167
|
const heading = line.match(/^(#{1,6})\s+(.*)$/);
|
|
104
168
|
if (heading) {
|
|
105
169
|
const level = heading[1].length;
|
|
106
170
|
if (excludedAt !== null && level > excludedAt) continue; // still inside the excluded section
|
|
171
|
+
if (decisionsAt !== null && level > decisionsAt) continue; // a subsection of Design Decisions
|
|
107
172
|
// A heading both ends the previous section and decides whether this one declares or claims.
|
|
108
173
|
// Strip an ordinal prefix ("## 5. Known Issues", "## 8. Viewpoint Grouping") before
|
|
109
174
|
// testing. The regex anchors at the start, so a NUMBERED heading matched nothing and
|
|
110
175
|
// every excluded section leaked its rows back in as claims — a real viewpoint numbers
|
|
111
176
|
// its sections, so in practice the exclusion list was doing nothing at all (#657).
|
|
112
|
-
|
|
177
|
+
// `Phần 4:` / `Part 4.` / `Mục 4.` number a section the same way.
|
|
178
|
+
const headText = heading[2].replace(/[*`]/g, '').replace(/^\s*(?:(?:[Pp]hần|[Pp]art|[Mm]ục|[Ss]ection)\s+)?(?:\d+|[A-Z])\s*[.):]\s*/, '').trim();
|
|
113
179
|
inDeclarationSection = DECLARATION_SECTION.test(headText);
|
|
114
180
|
excludedAt = inDeclarationSection ? level : null;
|
|
181
|
+
decisionsAt = DECISIONS_SECTION.test(headText) ? level : null;
|
|
115
182
|
continue;
|
|
116
183
|
}
|
|
117
184
|
if (inDeclarationSection) continue;
|
|
118
185
|
let text = '';
|
|
119
186
|
let lead = '';
|
|
187
|
+
let declared: RegExpMatchArray | null = null;
|
|
120
188
|
const bullet = line.match(/^(?:[-*+]|\d+[.)])\s+(.*)$/);
|
|
121
|
-
if (bullet) {
|
|
189
|
+
if (bullet) {
|
|
190
|
+
text = bullet[1]; lead = bullet[1];
|
|
191
|
+
declared = lead.replace(/[*`]/g, '').match(BULLET_ID);
|
|
192
|
+
}
|
|
122
193
|
else if (line.startsWith('|')) { // table data row
|
|
123
194
|
if (/^\|[\s|:-]+\|?$/.test(line)) continue; // separator
|
|
195
|
+
// The row right above a `|---|` separator IS the header, whatever its words (`Skill #`,
|
|
196
|
+
// `Case #`) — structure, not a guessed word list, so a data row such as `C#` or `Issue #`
|
|
197
|
+
// is never mistaken for one (#832). The word list below still covers a separator-less table.
|
|
198
|
+
const next = (lines[i + 1] ?? '').trim();
|
|
199
|
+
if (/^\|[\s|:-]+\|?$/.test(next) && next.includes('-')) continue; // header
|
|
124
200
|
const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
|
|
125
201
|
if (/^(vp|id|viewpoint|priority|reason|no\.?|category|item|trigger|#|pattern|applicable|notes|field|constraint|code|description|status|step|flow|ref|level|question|screen|actor|branches from|own steps|component|thành phần|trường|bước)$/i.test(cells[0] || '')) continue; // header
|
|
126
202
|
text = cells.join(' — ');
|
|
127
|
-
lead
|
|
203
|
+
// The lead is the row's FIRST cell, empty or not: a continuation row (`| | | case | … see SG-02 |`)
|
|
204
|
+
// declares nothing. A later cell holding an id and nothing else still declares (`| x | VP-UI-001 |`),
|
|
205
|
+
// and so does a row number followed by a cell that starts with one (`| 1 | VP-UI-001 — … |`).
|
|
206
|
+
const raw = line.replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.replace(/[*`]/g, '').trim());
|
|
207
|
+
lead = raw[0] ?? '';
|
|
208
|
+
declared = lead.match(ID_RE)
|
|
209
|
+
?? (/^\d+\.?$/.test(lead) ? (raw[1] ?? '').match(BULLET_ID) : null)
|
|
210
|
+
?? raw.slice(1).map((c) => c.match(WHOLE_ID)).find(Boolean) ?? null;
|
|
128
211
|
} else continue;
|
|
129
212
|
text = text.replace(/[*`]/g, '').trim();
|
|
130
213
|
if (!text) continue;
|
|
@@ -139,15 +222,16 @@ export function parseViewpointItems(viewpointPath: string): { id?: string; text:
|
|
|
139
222
|
// under a grouping section. It declares nothing; it points at things declared elsewhere.
|
|
140
223
|
const leadIds = new Set((leadClean.match(new RegExp(ID_RE.source, 'g')) || []).filter((x) => /\d/.test(x)));
|
|
141
224
|
if (leadIds.size > 1) continue;
|
|
142
|
-
const
|
|
143
|
-
const id = idM && /\d/.test(idM[1]) ? idM[1] : undefined; // require a digit so prose words aren't IDs
|
|
225
|
+
const id = declared && /\d/.test(declared[1]) ? declared[1] : undefined; // require a digit so prose words aren't IDs
|
|
144
226
|
// A row keyed by a bare GROUP prefix ("VP-LOGIC — transitions — High — why the flow exists")
|
|
145
227
|
// declares a family and its priority, not a checklist item: nothing can "cover" it, and
|
|
146
228
|
// demanding coverage sends the author looking for a test that was never meant to exist.
|
|
147
229
|
if (/^VP-[A-Z]+\s*(?:—|-|\||$)/.test(leadClean) && !/\d/.test(leadClean)) continue;
|
|
148
230
|
const words = (text.toLowerCase().match(/[a-z][a-z-]{3,}/g) || []).filter((w) => !GENERIC.has(w));
|
|
149
231
|
if (!id && words.length < 2) continue; // not substantive enough to track
|
|
150
|
-
items.push(
|
|
232
|
+
items.push(decisionsAt !== null && id && DECISION_ID.test(id)
|
|
233
|
+
? { id, text: text.slice(0, 100), decision: true }
|
|
234
|
+
: { id, text: text.slice(0, 100) });
|
|
151
235
|
}
|
|
152
236
|
return items;
|
|
153
237
|
}
|
|
@@ -158,6 +242,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
|
|
|
158
242
|
// (#622) — silently collapsing those would remove its input.
|
|
159
243
|
const seen = new Set<string>();
|
|
160
244
|
const items = parseViewpointItems(viewpointPath)
|
|
245
|
+
.filter((i) => !i.decision)
|
|
161
246
|
.filter((i) => { const k = `${i.id ?? ''}|${i.text}`; if (seen.has(k)) return false; seen.add(k); return true; });
|
|
162
247
|
if (!fs.existsSync(viewpointPath) || items.length === 0) {
|
|
163
248
|
return { hasViewpoint: fs.existsSync(viewpointPath), total: 0, covered: 0, ratio: 1, missing: [], manualOnly: [], partial: [] };
|
|
@@ -191,8 +276,7 @@ export function viewpointLedger(viewpointPath: string, scenarios: ScenarioInfo[]
|
|
|
191
276
|
const n = claimedCount(item.text);
|
|
192
277
|
const auto = carriers.filter((s) => !s.manual);
|
|
193
278
|
if (n !== null && auto.length > 0 && !auto.some((s) => /\bsee all\b/.test(s.stepsText))) {
|
|
194
|
-
const asserted = Math.max(...auto.map((s) =>
|
|
195
|
-
(s.stepsText.match(/\bsee \[[^\]]+\][^|]*?\b(?:with|contains) \{\{/g) ?? []).length));
|
|
279
|
+
const asserted = Math.max(...auto.map((s) => countedAssertions(s.stepsText)));
|
|
196
280
|
if (asserted > 0 && asserted < n) partial.push({ id: item.id, text: item.text, claimed: n, asserted });
|
|
197
281
|
}
|
|
198
282
|
}
|
|
@@ -349,13 +433,16 @@ export function viewpointIntegrity(
|
|
|
349
433
|
}
|
|
350
434
|
|
|
351
435
|
// The Priority-Viewpoints table: `| <id> | <priority> | <reason> |`.
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
.matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)]
|
|
436
|
+
const vpText = readViewpointText(viewpointPath);
|
|
437
|
+
const words = [...vpText
|
|
438
|
+
.matchAll(new RegExp(`^\\|\\s*((?:VP|FL)[A-Z0-9._-]*)\\s*\\|\\s*(${PRIORITY_WORDS})\\s*\\|`, 'gim'))]
|
|
356
439
|
.map((m) => m[2].toLowerCase());
|
|
357
|
-
|
|
358
|
-
|
|
440
|
+
// Fold only synonyms (Medium / Med / Normal are one level). `critical` stays its own word: a
|
|
441
|
+
// Critical / High / High table is a ranking, and folding it into `high` made it read as uniform.
|
|
442
|
+
const level = (w: string): string => (/^(?:medium|med|normal)$/.test(w) ? 'normal' : w);
|
|
443
|
+
const uniformPriority = words.length >= 3 && new Set(words.map(level)).size === 1
|
|
444
|
+
? { levels: [words[0]], rows: words.length } // the word QA wrote, not the folded key
|
|
445
|
+
: null;
|
|
359
446
|
|
|
360
447
|
return { duplicateIds, danglingRefs, uniformPriority };
|
|
361
448
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which create-test mode a unit's test-viewpoint.md calls for — the table in create-test.md,
|
|
3
|
+
* computed so the agent reads it from `sungen viewpoint scan` instead of inferring it.
|
|
4
|
+
*
|
|
5
|
+
* mode 2 (AI drafts / re-reviews its draft): file missing · untouched scaffold · first line
|
|
6
|
+
* `drafted-by: ai` · VP rows exist but none has a Kỳ vọng
|
|
7
|
+
* mode 1 (review only, never edit): anything else — a QA-written file, with or without VP ids
|
|
8
|
+
*
|
|
9
|
+
* "Untouched scaffold" is checked on RAW lines against the template the scaffold is written from:
|
|
10
|
+
* apart from the first `# ` line (the title), every non-blank line must be a template line —
|
|
11
|
+
* comments included, so a QA note inside `<!-- -->` or under its own `# ` heading makes the file
|
|
12
|
+
* the QA's (owner decision 2026-10-14: a QA file is never overwritten). Every doubt falls to
|
|
13
|
+
* mode 1: a scaffold from an older sungen (other template lines) or an unreadable template.
|
|
14
|
+
*/
|
|
15
|
+
import { isAiDraft } from './viewpoint-draft-header';
|
|
16
|
+
import { viewpointScaffold } from '../orchestrator/viewpoint-scaffold';
|
|
17
|
+
|
|
18
|
+
export type ViewpointMode = 1 | 2;
|
|
19
|
+
|
|
20
|
+
/** Trimmed non-blank lines without the title (the first `# ` line), BOM and CRLF ignored. */
|
|
21
|
+
function bodyLines(text: string): string[] {
|
|
22
|
+
const lines = text.replace(/^/, '').replace(/\r\n?/g, '\n').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
23
|
+
const title = lines.findIndex((l) => l.startsWith('# '));
|
|
24
|
+
if (title >= 0) lines.splice(title, 1);
|
|
25
|
+
return lines;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let templateLines: Set<string> | null = null;
|
|
29
|
+
function isUntouchedScaffold(raw: string): boolean {
|
|
30
|
+
if (!templateLines) {
|
|
31
|
+
try {
|
|
32
|
+
templateLines = new Set([false, true].flatMap((mobile) => bodyLines(viewpointScaffold('', mobile))));
|
|
33
|
+
} catch {
|
|
34
|
+
return false; // no template to compare with → treat the file as the QA's
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const known = templateLines;
|
|
38
|
+
return bodyLines(raw).every((l) => known.has(l));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `raw` is the file as written (null when missing); `rows` are its VP rows from the scan. */
|
|
42
|
+
export function viewpointMode(raw: string | null, rows: { hasExpected: boolean }[]): { mode: ViewpointMode; reason: string } {
|
|
43
|
+
if (raw === null) return { mode: 2, reason: 'file missing' };
|
|
44
|
+
if (isAiDraft(raw)) return { mode: 2, reason: 'drafted-by: ai (re-review)' };
|
|
45
|
+
if (rows.length > 0) {
|
|
46
|
+
return rows.some((r) => r.hasExpected) ? { mode: 1, reason: 'QA-written' } : { mode: 2, reason: 'VP rows without Kỳ vọng' };
|
|
47
|
+
}
|
|
48
|
+
return isUntouchedScaffold(raw) ? { mode: 2, reason: 'untouched scaffold' } : { mode: 1, reason: 'QA-written, no VP ids' };
|
|
49
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One reading of a priority — a viewpoint row's cell or a scenario's tag.
|
|
3
|
+
*
|
|
4
|
+
* The overview parser, the ledger's uniform-priority check and PRIORITY-CONTRADICTED each read a
|
|
5
|
+
* priority their own way (`/high/i` substrings, raw words, a one-off medium→normal), so the same
|
|
6
|
+
* row could be `Medium` to one and `Unknown` to another — `Med`, which QA writes, was `Unknown`.
|
|
7
|
+
* The vocabulary is `high | normal | low`; `critical`, `medium` and `med` are read as aliases.
|
|
8
|
+
*/
|
|
9
|
+
export type PriorityLevel = 'high' | 'normal' | 'low' | 'deferred' | 'skip' | 'unknown';
|
|
10
|
+
|
|
11
|
+
const WORD: Record<string, PriorityLevel> = {
|
|
12
|
+
critical: 'high', high: 'high',
|
|
13
|
+
medium: 'normal', med: 'normal', normal: 'normal',
|
|
14
|
+
low: 'low', deferred: 'deferred',
|
|
15
|
+
};
|
|
16
|
+
/** "Declared not applicable" — one level, so every consumer checks one word. */
|
|
17
|
+
const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A priority cell or tag → its level. The whole cell being a word is the clean case; otherwise the
|
|
21
|
+
* overview has always read prose by SUBSTRING in precedence order (`High — complex form`,
|
|
22
|
+
* `Medium-High`), and keeps doing so. The substring words stay unbounded for `high|medium|low`
|
|
23
|
+
* (existing behaviour); the newer `critical|normal|med` need word boundaries (`Medical` is not `Med`).
|
|
24
|
+
*/
|
|
25
|
+
export function parsePriority(cell: string): PriorityLevel {
|
|
26
|
+
const w = cell.replace(/[*_`]/g, '').replace(/\.+$/, '').trim().toLowerCase();
|
|
27
|
+
if (WORD[w]) return WORD[w];
|
|
28
|
+
if (NOT_APPLICABLE.test(w)) return 'skip';
|
|
29
|
+
if (/high|\bcritical\b/.test(w)) return 'high';
|
|
30
|
+
if (/medium|\bnormal\b|\bmed\b/.test(w)) return 'normal';
|
|
31
|
+
if (/low/.test(w)) return 'low';
|
|
32
|
+
return 'unknown';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The public `ViewpointEntry.priority` spelling of a level — the exported type is unchanged. */
|
|
36
|
+
export function toEntryPriority(level: PriorityLevel): 'High' | 'Medium' | 'Low' | 'Unknown' {
|
|
37
|
+
return level === 'high' ? 'High' : level === 'normal' ? 'Medium' : level === 'low' ? 'Low' : 'Unknown';
|
|
38
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VIEWPOINT-REVIEW-OPEN (info) — the audit's one line about `requirements/viewpoint-review.md`.
|
|
3
|
+
*
|
|
4
|
+
* create-test reviews test-viewpoint.md with the sungen-viewpoint checklist and writes each point a
|
|
5
|
+
* QA should look at as a table row whose last cell is `[ ]`; the QA ticks it `[x]` or deletes the
|
|
6
|
+
* row. While rows stay unticked the audit says how many, once. Advisory only: no score, no gate, no
|
|
7
|
+
* baseline effect. A missing or unreadable file says nothing — the review is optional input.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { stripHtmlComments } from './read-text';
|
|
12
|
+
|
|
13
|
+
export const VIEWPOINT_REVIEW_FILE = 'viewpoint-review.md';
|
|
14
|
+
|
|
15
|
+
const SEPARATOR = /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/;
|
|
16
|
+
/** A table separator, not a `---` rule under a row. */
|
|
17
|
+
const isSeparator = (line: string): boolean => SEPARATOR.test(line) && line.includes('|');
|
|
18
|
+
const UNTICKED = /^\[\s?\]$/;
|
|
19
|
+
|
|
20
|
+
/** The last cell of a markdown table row, or null for a line that is not one. */
|
|
21
|
+
function lastCell(line: string): string | null {
|
|
22
|
+
const t = line.trim();
|
|
23
|
+
if (!t.startsWith('|') || !t.endsWith('|') || t.length < 2) return null;
|
|
24
|
+
const cells = t.slice(1, -1).split(/(?<!\\)\|/);
|
|
25
|
+
return cells[cells.length - 1].trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Body rows whose last cell is `[ ]`. The header row (`… | [ ] |` above `|---|`) is not a point. */
|
|
29
|
+
export function openReviewPoints(text: string): number {
|
|
30
|
+
const lines = stripHtmlComments(text.replace(/\r\n?/g, '\n')).split('\n');
|
|
31
|
+
let open = 0;
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
if (isSeparator(lines[i]) || isSeparator(lines[i + 1] ?? '')) continue;
|
|
34
|
+
const last = lastCell(lines[i]);
|
|
35
|
+
if (last !== null && UNTICKED.test(last)) open++;
|
|
36
|
+
}
|
|
37
|
+
return open;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The info finding for one unit's `requirements/` directory; empty when nothing is open. */
|
|
41
|
+
export function viewpointReviewOpenFindings(requirementsDir: string): string[] {
|
|
42
|
+
let text: string;
|
|
43
|
+
try {
|
|
44
|
+
text = fs.readFileSync(path.join(requirementsDir, VIEWPOINT_REVIEW_FILE), 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const open = openReviewPoints(text);
|
|
49
|
+
if (open === 0) return [];
|
|
50
|
+
return [`VIEWPOINT-REVIEW-OPEN (info): ${open} open point(s) in requirements/${VIEWPOINT_REVIEW_FILE} — the checklist review of test-viewpoint.md is waiting for QA. QA: fix the viewpoint where you agree, then tick the row \`[x]\` or delete it. Not scored; an agent does not tick rows for the QA.`];
|
|
51
|
+
}
|