@sun-asterisk/sungen 3.2.34-beta.1 → 3.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +3 -29
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/index.js +0 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/test-data-resolver.d.ts.map +1 -1
- package/dist/exporters/test-data-resolver.js +12 -2
- package/dist/exporters/test-data-resolver.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +1 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
- package/dist/generators/test-generator/code-generator.js +4 -5
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +9 -3
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +9 -3
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +8 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +52 -8
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +16 -0
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +59 -0
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +10 -2
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +2 -13
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/next-step.d.ts +0 -2
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +1 -15
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +3 -6
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +4 -9
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +9 -11
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
- package/dist/harness/viewpoint-baseline.js +1 -7
- package/dist/harness/viewpoint-baseline.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +3 -13
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +8 -38
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -1
- package/dist/orchestrator/ai-skills/commands.js +5 -3
- package/dist/orchestrator/ai-skills/commands.js.map +1 -1
- package/dist/orchestrator/ai-skills/golden-skills.test.js +29 -0
- package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -1
- package/dist/orchestrator/flow-manager.d.ts +1 -0
- package/dist/orchestrator/flow-manager.d.ts.map +1 -1
- package/dist/orchestrator/flow-manager.js +52 -3
- 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 +52 -4
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +22 -30
- package/dist/orchestrator/templates/ai-src/config/claude.md +20 -7
- package/dist/orchestrator/templates/ai-src/config/copilot.md +20 -7
- package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
- package/dist/orchestrator/templates/specs-test-data.ts +22 -4
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +1 -4
- package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -1
- package/dist/orchestrator/viewpoint-pattern-catalog.js +2 -5
- package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -1
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +3 -26
- package/src/cli/index.ts +0 -2
- package/src/exporters/test-data-resolver.ts +10 -2
- package/src/generators/gherkin-parser/index.ts +1 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
- package/src/generators/test-generator/code-generator.ts +4 -4
- package/src/generators/test-generator/diagnostics.ts +9 -3
- package/src/generators/test-generator/patterns/types.ts +2 -0
- package/src/generators/test-generator/step-mapper.ts +56 -9
- package/src/generators/test-generator/utils/data-resolver.ts +53 -0
- package/src/generators/test-generator/utils/selector-resolver.ts +10 -2
- package/src/harness/audit.ts +2 -13
- package/src/harness/next-step.ts +1 -17
- package/src/harness/parse.ts +5 -13
- package/src/harness/sensors.ts +8 -9
- package/src/harness/viewpoint-baseline.ts +1 -7
- package/src/harness/viewpoint-ledger.ts +11 -52
- package/src/index.ts +2 -1
- package/src/orchestrator/ai-skills/commands.ts +5 -3
- package/src/orchestrator/ai-skills/golden-skills.test.ts +23 -0
- package/src/orchestrator/flow-manager.ts +53 -3
- package/src/orchestrator/screen-manager.ts +52 -4
- package/src/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
- package/src/orchestrator/templates/ai-src/commands/create-test.md +22 -30
- package/src/orchestrator/templates/ai-src/config/claude.md +20 -7
- package/src/orchestrator/templates/ai-src/config/copilot.md +20 -7
- package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
- package/src/orchestrator/templates/specs-test-data.ts +22 -4
- package/src/orchestrator/viewpoint-pattern-catalog.ts +2 -5
- package/dist/cli/commands/viewpoint.d.ts +0 -7
- package/dist/cli/commands/viewpoint.d.ts.map +0 -1
- package/dist/cli/commands/viewpoint.js +0 -70
- package/dist/cli/commands/viewpoint.js.map +0 -1
- package/dist/harness/viewpoint-draft-header.d.ts +0 -7
- package/dist/harness/viewpoint-draft-header.d.ts.map +0 -1
- package/dist/harness/viewpoint-draft-header.js +0 -100
- package/dist/harness/viewpoint-draft-header.js.map +0 -1
- package/dist/harness/viewpoint-mode.d.ts +0 -9
- package/dist/harness/viewpoint-mode.d.ts.map +0 -1
- package/dist/harness/viewpoint-mode.js +0 -52
- package/dist/harness/viewpoint-mode.js.map +0 -1
- package/dist/harness/viewpoint-priority.d.ts +0 -19
- package/dist/harness/viewpoint-priority.d.ts.map +0 -1
- package/dist/harness/viewpoint-priority.js +0 -36
- package/dist/harness/viewpoint-priority.js.map +0 -1
- package/dist/harness/viewpoint-review-open.d.ts +0 -6
- package/dist/harness/viewpoint-review-open.d.ts.map +0 -1
- package/dist/harness/viewpoint-review-open.js +0 -90
- package/dist/harness/viewpoint-review-open.js.map +0 -1
- package/dist/harness/viewpoint-scan-refs.d.ts +0 -50
- package/dist/harness/viewpoint-scan-refs.d.ts.map +0 -1
- package/dist/harness/viewpoint-scan-refs.js +0 -200
- package/dist/harness/viewpoint-scan-refs.js.map +0 -1
- package/dist/harness/viewpoint-scan.d.ts +0 -41
- package/dist/harness/viewpoint-scan.d.ts.map +0 -1
- package/dist/harness/viewpoint-scan.js +0 -118
- package/dist/harness/viewpoint-scan.js.map +0 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
- package/dist/orchestrator/viewpoint-scaffold.d.ts +0 -3
- package/dist/orchestrator/viewpoint-scaffold.d.ts.map +0 -1
- package/dist/orchestrator/viewpoint-scaffold.js +0 -70
- package/dist/orchestrator/viewpoint-scaffold.js.map +0 -1
- package/src/cli/commands/viewpoint.ts +0 -58
- package/src/harness/viewpoint-draft-header.ts +0 -55
- package/src/harness/viewpoint-mode.ts +0 -49
- package/src/harness/viewpoint-priority.ts +0 -38
- package/src/harness/viewpoint-review-open.ts +0 -51
- package/src/harness/viewpoint-scan-refs.ts +0 -154
- package/src/harness/viewpoint-scan.ts +0 -111
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
- package/src/orchestrator/viewpoint-scaffold.ts +0 -36
package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs
CHANGED
|
@@ -1 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
{{#if guardFillable~}}
|
|
2
|
+
{{!-- A recipe-less date-picker / button / link: Playwright's own "Element is not an <input>" names
|
|
3
|
+
no fix when the target is a trigger that opens a calendar popup. Keep Playwright's semantics
|
|
4
|
+
(it also fills a <label>'s control or a contenteditable child) and only re-word that one
|
|
5
|
+
failure with the way forward (#907). --}}
|
|
6
|
+
try {
|
|
7
|
+
await {{> locator}}.fill('{{escapeQuotes fillValue}}');
|
|
8
|
+
} catch (e) {
|
|
9
|
+
if (!/is not an <input>/.test(String(e))) throw e;
|
|
10
|
+
throw new Error('[sungen] [{{escapeQuotes selectorRef}}] is not a fillable element (a button or popup trigger opens a picker). Declare its choreography as a recipe with a fill intent (and an assertValue intent) in qa/app.yaml and point the selector at it with variant: <recipe>, or widgets.datepicker for every date-picker.');
|
|
11
|
+
}
|
|
12
|
+
{{else~}}
|
|
13
|
+
await {{> locator}}.fill('{{escapeQuotes fillValue}}');{{/if}}
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
{{#if emptyMeansEmpty~}}
|
|
2
|
+
{{!-- toContainText('') passes for ANY text — the empty string is a substring of everything — so an
|
|
3
|
+
"is empty" check would assert nothing. An empty expected value means the element shows no text (#910). --}}
|
|
4
|
+
{ const __expected = '{{escapeQuotes expectedText}}';
|
|
5
|
+
if (__expected === '') await expect({{> locator}}).toHaveText('');
|
|
6
|
+
else await expect({{> locator}}).toContainText(__expected);
|
|
7
|
+
}
|
|
8
|
+
{{else~}}
|
|
9
|
+
await expect({{> locator}}).toContainText('{{escapeQuotes expectedText}}');{{/if}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{{!-- `see [X] is expanded | is collapsed` (#911). Reads the signal expand-action.hbs drives — a class
|
|
2
|
+
on a descendant, a declared boolean attribute, else aria-expanded — but with no "only if the
|
|
3
|
+
element exposes it" escape: an assertion that can skip itself asserts nothing. --}}
|
|
4
|
+
{{#if expanded.class}}
|
|
5
|
+
{{#if (eq direction 'expand')}}
|
|
6
|
+
await expect({{> locator}}.locator('.{{escapeQuotes expanded.class}}')).toBeVisible();
|
|
7
|
+
{{~else~}}
|
|
8
|
+
await expect({{> locator}}.locator('.{{escapeQuotes expanded.class}}')).toHaveCount(0);
|
|
9
|
+
{{~/if}}
|
|
10
|
+
{{~else~}}
|
|
11
|
+
await expect({{> locator}}).toHaveAttribute('{{#if expanded.attribute}}{{escapeQuotes expanded.attribute}}{{else}}aria-expanded{{/if}}', '{{#if (eq direction 'expand')}}true{{else}}false{{/if}}');
|
|
12
|
+
{{~/if}}
|
|
@@ -1187,9 +1187,9 @@ export class CodeGenerator {
|
|
|
1187
1187
|
): { preconditions: CapabilityPrecondition[]; casesDataset?: string } {
|
|
1188
1188
|
this.stepMapper.setScenarioContext(stepsToMap);
|
|
1189
1189
|
|
|
1190
|
-
// Data-driven (@cases): the scenario's {{col}} refs are dataset *row columns*
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1190
|
+
// Data-driven (@cases): the scenario's {{col}} refs that are dataset *row columns* exist
|
|
1191
|
+
// only at runtime — they resolve to a runtime get() (→ rowData.get) instead of a compile-time
|
|
1192
|
+
// YAML lookup. A plain test-data key stays readable at compile time (#950).
|
|
1193
1193
|
const casesTag = scenario.tags.find((t) => t.startsWith('@cases:'));
|
|
1194
1194
|
const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
|
|
1195
1195
|
if (casesDataset) {
|
|
@@ -1197,7 +1197,7 @@ export class CodeGenerator {
|
|
|
1197
1197
|
for (const st of stepsToMap) {
|
|
1198
1198
|
for (const m of (st.text || '').matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) refs.add(m[1]);
|
|
1199
1199
|
}
|
|
1200
|
-
|
|
1200
|
+
this.stepMapper.bindCasesDataset(casesDataset, refs);
|
|
1201
1201
|
}
|
|
1202
1202
|
|
|
1203
1203
|
const preconditions = this.capabilityPreconditions(scenario);
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
* nothing for the query half to assert, so the step checks the PATH only. Not an
|
|
50
50
|
* error (the value may legitimately vary per environment), but the step asserts
|
|
51
51
|
* strictly less than it appears to
|
|
52
|
-
* SG-W015 — `see [X] card/item with {{n}}` where the value is a bare number:
|
|
52
|
+
* SG-W015 — `see [X] card/item with {{n}}` where the value is a bare number (in @cases: on
|
|
53
|
+
* any row), or `see [X] row with {{n}}` whose @cases rows mix numbers and text: this compiles
|
|
53
54
|
* to a TEXT assertion (toHaveText('3')), NOT a count — an author reaching for
|
|
54
55
|
* "3 cards are shown" gets an assertion that can only pass if the card's text
|
|
55
56
|
* IS the digit. Count semantics exist on row/table ("see [T] table with {{n}}",
|
|
@@ -67,13 +68,18 @@
|
|
|
67
68
|
* SG-E023 — the step carries an assertion clause (`matches "…"`, `contains`, `has text`, a state)
|
|
68
69
|
* but the generated code is a BARE VISIBILITY check: the clause was swallowed by the
|
|
69
70
|
* catch-all and the test would pass whatever the element says. Four separate defects
|
|
70
|
-
* have arrived this way, so the step compiles to a runtime throw instead.
|
|
71
|
+
* have arrived this way, so the step compiles to a runtime throw instead. Also a
|
|
72
|
+
* trailing `is <word>` / `is not <word>` that is no state (`is on`, `is open`,
|
|
73
|
+
* `is not visible`), other than visibility itself (#911).
|
|
71
74
|
* SG-W017 — a step needs a selector-entry field the entry does not declare, so there is nothing
|
|
72
75
|
* honest to compile. Today: a table paginate step whose table declares no
|
|
73
|
-
* `pagination.next`/`.prev` (#712)
|
|
76
|
+
* `pagination.next`/`.prev` (#712); `is expanded`/`is collapsed` on an entry declaring
|
|
77
|
+
* `expanded.state: none` (#911). The mirror of SG-W004 — that one reports a field
|
|
74
78
|
* written that nothing reads, this one a field a consumer needs that nobody wrote.
|
|
75
79
|
* Compiles the step to a loud runtime throw rather than guessing a locator, which is
|
|
76
80
|
* the #690 failure (a locator no page has, failing far from the cause).
|
|
81
|
+
* Also a web `fill [X] date-picker` whose entry locates a `<button>` (a calendar
|
|
82
|
+
* trigger) and names no `fill` recipe — `.fill()` cannot type into a button (#907).
|
|
77
83
|
* SG-W018 — a selectors.yaml entry declares an `nth:` the compiler cannot use (#720). `nth:` was
|
|
78
84
|
* the one entry field whose VALUE nothing checked: the key is known (SG-W004 quiet) and
|
|
79
85
|
* the `type:` is valid (SG-E027 quiet), so `nth: -1`, `nth: 'last'`, `nth: 1.5` and
|
|
@@ -13,6 +13,9 @@ import { isKnownSelectorType } from './utils/selector-type-validation';
|
|
|
13
13
|
import { loadAppProfile, ResolvedAppProfile } from './app-profile';
|
|
14
14
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
15
15
|
|
|
16
|
+
/** Trailing `is <word>` that a visibility check honestly asserts (SG-E023 lets these through). */
|
|
17
|
+
const VISIBILITY_WORDS = new Set(['visible', 'displayed', 'shown', 'present']);
|
|
18
|
+
|
|
16
19
|
/**
|
|
17
20
|
* The correction SG-W001 owes a project that gated a platform driver away.
|
|
18
21
|
*
|
|
@@ -143,6 +146,10 @@ export class StepMapper {
|
|
|
143
146
|
this.featurePlatform = featurePlatform;
|
|
144
147
|
this.featurePath = featurePath;
|
|
145
148
|
this.selectorResolver.setFeatureContext(featureName);
|
|
149
|
+
// Scenario-scoped data state (captured vars, the bound @cases dataset) of the previous feature's
|
|
150
|
+
// last scenario must not reach this feature's Background, which is mapped before any scenario
|
|
151
|
+
// resets it: a column `{{k}}` there made a plain `{{k}}` here unreadable (#950).
|
|
152
|
+
this.dataResolver.clearCaptured();
|
|
146
153
|
// One StepMapper serves every feature in a run, so the step counter that names the
|
|
147
154
|
// generated temporaries (`__t_<n>`) used to carry over between files: a screen's spec
|
|
148
155
|
// then depended on how many steps the PRECEDING files happened to contain — i.e. on
|
|
@@ -212,6 +219,23 @@ export class StepMapper {
|
|
|
212
219
|
this.dataResolver.registerCaptured(name);
|
|
213
220
|
}
|
|
214
221
|
|
|
222
|
+
/**
|
|
223
|
+
* `@cases:<dataset>` — bind the scenario's dataset and register as runtime-only the refs that are
|
|
224
|
+
* its COLUMNS (each row supplies them). A plain key the base test-data defines stays a normal ref,
|
|
225
|
+
* so the compiler can still read its value (#950: registering every ref turned
|
|
226
|
+
* `row with {{rows_per_page}}` into a text assertion). A ref that is neither stays runtime-only,
|
|
227
|
+
* as before.
|
|
228
|
+
*/
|
|
229
|
+
bindCasesDataset(dataset: string, refs: Iterable<string>): void {
|
|
230
|
+
const columns = new Set(this.dataResolver.setCasesDataset(dataset, this.featureName));
|
|
231
|
+
for (const ref of refs) {
|
|
232
|
+
const isColumn = columns.has(ref.split(/[.[]/)[0]);
|
|
233
|
+
if (isColumn || this.dataResolver.peekBaseValue(ref, this.featureName) === undefined) {
|
|
234
|
+
this.dataResolver.registerCaptured(ref);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
215
239
|
/**
|
|
216
240
|
* Register the head of a variable bound by the feature's `@beforeAll` hook. Feature-scoped: it
|
|
217
241
|
* survives setScenarioContext() so every scenario can read `{{head.*}}` as a runtime get().
|
|
@@ -291,8 +315,11 @@ export class StepMapper {
|
|
|
291
315
|
/\bsees?\s+\[/.test(step.text) && /\bwith\b/.test(step.text)) {
|
|
292
316
|
// peekBaseValue reads the literal from test-data even in runtime-data mode,
|
|
293
317
|
// where resolveData returns an opaque marker (never numeric-looking).
|
|
318
|
+
// Every value the ref takes — a @cases column has one per row (#950).
|
|
294
319
|
let resolvedValue: string | undefined;
|
|
295
|
-
try {
|
|
320
|
+
try {
|
|
321
|
+
resolvedValue = this.dataResolver.peekValues(step.dataRef, this.featureName)?.find((v) => /^\d+$/.test(v.trim()));
|
|
322
|
+
} catch { /* unresolved at compile time → cannot judge */ }
|
|
296
323
|
if (resolvedValue !== undefined && /^\d+$/.test(resolvedValue.trim())) {
|
|
297
324
|
this.diagnostics.add('SG-W015',
|
|
298
325
|
`\`see [${step.selectorRef}] ${step.elementType} with {{${step.dataRef}}}\` compiles to a TEXT assertion toHaveText('${resolvedValue.trim()}'), NOT a count of ${step.elementType}s. For a count use \`see [Ref] row with {{n}}\` / \`see [T] table with {{n}}\` (or a list "should have count"); if '${resolvedValue.trim()}' really is the ${step.elementType}'s text, assert it on a text/label element instead`,
|
|
@@ -655,7 +682,8 @@ export class StepMapper {
|
|
|
655
682
|
if (!/\b(?:urlMatches|expectPage)\(/.test(mapped.code || '')) return; // both URL-assertion shapes (#662)
|
|
656
683
|
// The base value, never resolveData(): in runtime mode that returns the marker, which carries
|
|
657
684
|
// no query by construction — the diagnostic would then fire on every single step.
|
|
658
|
-
|
|
685
|
+
// Any value the ref takes — a @cases column has one per row (#950); the first without a query.
|
|
686
|
+
const value = this.dataResolver.peekValues(step.dataRef, this.featureName)?.find((v) => !/[?#]/.test(v));
|
|
659
687
|
if (value == null || /[?#]/.test(value)) return;
|
|
660
688
|
this.diagnostics.add('SG-W014',
|
|
661
689
|
`{{${step.dataRef}}} = "${value}" carries no query and no fragment, so this page assertion checks the PATH only. Pass a value like "?q=…" (or drop the "with" and let the step assert a bare page).`,
|
|
@@ -856,13 +884,32 @@ export class StepMapper {
|
|
|
856
884
|
[/\b(?:should\s+have|has)\s+text\b/i, 'has text'],
|
|
857
885
|
];
|
|
858
886
|
const hit = CLAUSES.find(([re]) => re.test(step.text));
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
887
|
+
// A trailing `is <word>` that is no state reached a bare visibility check the same way:
|
|
888
|
+
// `see [Start Date] button is expanded` asserted only that the button was shown, and passed with
|
|
889
|
+
// the calendar closed (#911); `is not visible` even asserted the OPPOSITE. Known states are
|
|
890
|
+
// SG-E021's (`step.state` is set). Only what a visibility check honestly asserts passes:
|
|
891
|
+
// `is visible` and its synonyms, and `is not hidden`. Storage's `is not empty` never gets here —
|
|
892
|
+
// this guard sees bare visibility code only.
|
|
893
|
+
let state: string | undefined;
|
|
894
|
+
if (!hit && !step.state) {
|
|
895
|
+
const m = maskRefsForMatching(step.text).replace(/\s+/g, ' ').replace(/ ?\.$/, '').toLowerCase()
|
|
896
|
+
.match(/ (?:is|are)( not| no longer)? ([a-z][\w-]*)$/);
|
|
897
|
+
const honest = m && (m[1] ? m[2] === 'hidden' : VISIBILITY_WORDS.has(m[2]));
|
|
898
|
+
if (m && !honest) state = `is${m[1] ?? ''} ${m[2]}`;
|
|
899
|
+
}
|
|
900
|
+
if (!hit && !state) return null;
|
|
901
|
+
|
|
902
|
+
const reason = state
|
|
903
|
+
? `step "${step.text}" ends in \`${state}\`, which is not a sungen state, but compiled to a bare `
|
|
904
|
+
+ `visibility check — the words were dropped, so the step would pass whenever the element is `
|
|
905
|
+
+ `shown. Use a state the compiler asserts (is visible / is hidden / is disabled / is enabled / `
|
|
906
|
+
+ `is checked / is unchecked / is selected / is focused / is empty / is expanded / is collapsed …, `
|
|
907
|
+
+ `see the sungen-gherkin-syntax States list), or tag the scenario @manual.`
|
|
908
|
+
: `step "${step.text}" carries the clause \`${hit![1]}\` but compiled to a bare `
|
|
909
|
+
+ `visibility check — the clause was dropped, so the assertion would pass whatever the element `
|
|
910
|
+
+ `says. Either the phrasing is not the supported one for that clause (see the `
|
|
911
|
+
+ `sungen-gherkin-syntax reference), or this sungen predates the step: check you are running `
|
|
912
|
+
+ `the project-local compiler (\`npx sungen\`), not an older global one.`;
|
|
866
913
|
this.diagnostics?.add('SG-E023', reason, { feature: this.featureName, step: step.text });
|
|
867
914
|
return {
|
|
868
915
|
code: unsupportedStepCode(reason),
|
|
@@ -54,6 +54,7 @@ export class DataResolver {
|
|
|
54
54
|
/** Clear captured vars — call at the start of each scenario. */
|
|
55
55
|
clearCaptured(): void {
|
|
56
56
|
this.capturedVars.clear();
|
|
57
|
+
this.casesRows = undefined;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
// Vars bound by a `@beforeAll` hook's `@api:`/`@query:` preconditions. The hook runs before every
|
|
@@ -131,6 +132,58 @@ export class DataResolver {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/** The `@cases:<name>` dataset rows of the scenario being compiled (scenario-scoped). */
|
|
136
|
+
private casesRows?: Array<Record<string, unknown>>;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Bind the scenario's `@cases` dataset so its values can be judged at compile time, and return its
|
|
140
|
+
* column names (empty when the dataset cannot be read — the caller then treats every ref as
|
|
141
|
+
* runtime-only, as before #950).
|
|
142
|
+
*/
|
|
143
|
+
setCasesDataset(dataset: string | undefined, featureName?: string): string[] {
|
|
144
|
+
this.casesRows = undefined;
|
|
145
|
+
if (!dataset || !featureName) return [];
|
|
146
|
+
try {
|
|
147
|
+
const rows = this.loadDataFile(featureName, dataset)?.[dataset];
|
|
148
|
+
if (Array.isArray(rows) && rows.length) {
|
|
149
|
+
// A scalar row is the `value` column — the runtime cases() wraps it the same way.
|
|
150
|
+
this.casesRows = rows.map((r) => (r && typeof r === 'object' && !Array.isArray(r) ? r : { value: r }));
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// unreadable dataset → no columns known
|
|
154
|
+
}
|
|
155
|
+
return this.casesRows ? [...new Set(this.casesRows.flatMap((r) => Object.keys(r)))] : [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Every value a ref can take at run time, for compile-time judgements: the base value of a plain
|
|
160
|
+
* key, or — for a column of the scenario's `@cases` dataset — the value on EACH row (a row that
|
|
161
|
+
* lacks the column falls back to the base key, as `rowData.get()` does). A dataset column used to
|
|
162
|
+
* be opaque here, so `row with {{n}}` lost its count reading in @cases and the SG-W014 / SG-W015
|
|
163
|
+
* checks went silent (#950). Undefined when any value cannot be read.
|
|
164
|
+
*/
|
|
165
|
+
peekValues(dataRef: string, featureName?: string): string[] | undefined {
|
|
166
|
+
const head = dataRef.split(/[.[]/)[0];
|
|
167
|
+
if (!this.casesRows?.some((row) => head in row)) {
|
|
168
|
+
const base = this.peekBaseValue(dataRef, featureName);
|
|
169
|
+
return base === undefined ? undefined : [base];
|
|
170
|
+
}
|
|
171
|
+
const out: string[] = [];
|
|
172
|
+
for (const row of this.casesRows) {
|
|
173
|
+
let value: unknown;
|
|
174
|
+
if (head in row) {
|
|
175
|
+
value = row;
|
|
176
|
+
for (const key of dataRef.split('.')) value = value && typeof value === 'object' ? (value as Record<string, unknown>)[key] : undefined;
|
|
177
|
+
} else {
|
|
178
|
+
try { value = this.resolveValue(dataRef, featureName); } catch { return undefined; }
|
|
179
|
+
}
|
|
180
|
+
if (typeof value === 'number' || typeof value === 'boolean') value = String(value);
|
|
181
|
+
if (typeof value !== 'string') return undefined;
|
|
182
|
+
out.push(value);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
134
187
|
private resolveValue(dataRef: string, featureName?: string): string {
|
|
135
188
|
const { fileName, valuePath } = this.parseDataRef(dataRef, featureName);
|
|
136
189
|
const data = this.loadDataFile(fileName, dataRef);
|
|
@@ -1024,8 +1024,8 @@ export class SelectorResolver {
|
|
|
1024
1024
|
// `type: locator` had to be used instead, which no page of the reference says (issue #690).
|
|
1025
1025
|
//
|
|
1026
1026
|
// Handled HERE, above the generic `locator:`-field branch below, because that branch returns
|
|
1027
|
-
// strategy 'css'
|
|
1028
|
-
//
|
|
1027
|
+
// strategy 'css' — putting `upload` in the switch left the two spellings diverging for any
|
|
1028
|
+
// entry that sets `locator:`. `withExtras` still stamps the declared
|
|
1029
1029
|
// `selectorType`, so a consumer that needs to tell an upload from a plain locator still can.
|
|
1030
1030
|
//
|
|
1031
1031
|
// `exact` is inert for the upload action itself (`setInputFiles` takes no text filter), but it
|
|
@@ -1049,6 +1049,7 @@ export class SelectorResolver {
|
|
|
1049
1049
|
value: locator,
|
|
1050
1050
|
locator: locator,
|
|
1051
1051
|
nth,
|
|
1052
|
+
exact,
|
|
1052
1053
|
});
|
|
1053
1054
|
}
|
|
1054
1055
|
|
|
@@ -1113,11 +1114,18 @@ export class SelectorResolver {
|
|
|
1113
1114
|
return resolvedRole;
|
|
1114
1115
|
}
|
|
1115
1116
|
|
|
1117
|
+
// `exact` carries no meaning for getByTestId itself, but text assertions on the entry read it
|
|
1118
|
+
// (`label with {{v}}` → toHaveText vs toContainText). Dropping it here made `exact: true`
|
|
1119
|
+
// inert on a testid entry while working on every other identity strategy (#910).
|
|
1116
1120
|
case 'testid':
|
|
1121
|
+
// `exact` travels with a testid entry too: the getByTestId locator ignores it, but a step
|
|
1122
|
+
// that compares text (`label with {{v}}`) reads it to pick toHaveText over toContainText.
|
|
1123
|
+
// It used to be dropped here alone, so `exact: true` on a testid entry did nothing (#910).
|
|
1117
1124
|
return withExtras({
|
|
1118
1125
|
strategy: 'testid',
|
|
1119
1126
|
value,
|
|
1120
1127
|
nth,
|
|
1128
|
+
exact,
|
|
1121
1129
|
});
|
|
1122
1130
|
|
|
1123
1131
|
case 'label':
|
package/src/harness/audit.ts
CHANGED
|
@@ -33,8 +33,6 @@ import { capabilityRegistry } from '../capabilities/registry';
|
|
|
33
33
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
34
34
|
import { contextRouter } from '../capabilities/context-router';
|
|
35
35
|
import { readTextFile, readViewpointText } from './read-text';
|
|
36
|
-
import { isAiDraft } from './viewpoint-draft-header';
|
|
37
|
-
import { viewpointReviewOpenFindings } from './viewpoint-review-open';
|
|
38
36
|
import { uiInventoryCoverage, uiInventoryFindings, UiInventoryResult } from './ui-inventory';
|
|
39
37
|
import { defectLinkFindings } from './defects';
|
|
40
38
|
|
|
@@ -204,9 +202,6 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
204
202
|
const viewpointBaseline = checkViewpointBaseline(
|
|
205
203
|
projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
|
|
206
204
|
const viewpointText = readViewpointText(viewpointPath);
|
|
207
|
-
// `drafted-by: ai` — unreviewed until `--accept-viewpoint` removes it. Only a draft that declares ids
|
|
208
|
-
// has anything to accept; an id-less one is VIEWPOINT-MISSING's to report, not a dead-end finding.
|
|
209
|
-
const aiDraft = isAiDraft(viewpointText) && viewpointBaseline.status !== 'absent';
|
|
210
205
|
// #569 — a flow WITH a contract is measured against ITS declaration, not a screen's.
|
|
211
206
|
// Keyword page-type detection force-fit flows into screen checklists (a registration
|
|
212
207
|
// flow judged as a `form` screen scored coverage 0%), so for contract-holding flows the
|
|
@@ -356,8 +351,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
356
351
|
// so it can only measure what their author already thought of. One field run wrote the file
|
|
357
352
|
// after generating the suite and said so in its own header — and both axes still read 100%,
|
|
358
353
|
// because a first audit records the baseline silently (#622). Untrusted until a human accepts it.
|
|
359
|
-
|
|
360
|
-
const viewpointMoved = viewpointBaseline.status === 'changed' || adoptedPostHoc || aiDraft;
|
|
354
|
+
const viewpointMoved = viewpointBaseline.status === 'changed' || adoptedPostHoc;
|
|
361
355
|
// #618 — is there a YARDSTICK at all? `atomicLedger` and `traceability` are both measured
|
|
362
356
|
// against test-viewpoint.md, and four more sensors read it (the ledger, the declaration-integrity
|
|
363
357
|
// checks, the browser-gesture check, the continuity check). When the file is absent every one of
|
|
@@ -671,12 +665,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
671
665
|
} else if (balance.imbalanced && !flowScored) {
|
|
672
666
|
findings.push(`BALANCE: ${balance.note} Stop expanding secondary viewpoints until business-core gaps are filled.`);
|
|
673
667
|
}
|
|
674
|
-
if (
|
|
675
|
-
findings.push(`VIEWPOINT-DRAFT-UNREVIEWED: \`requirements/test-viewpoint.md\` is marked \`drafted-by: ai\` and no QA has accepted it, so it is not recorded as the baseline and \`atomicLedger\` + \`traceability\` are held as unverified — a checklist the AI wrote would otherwise score the suite it generated at 100% by construction. QA: review the claims (add what you care about, delete what you do not), then \`sungen audit --screen ${screenName} --accept-viewpoint\` — it records the baseline and removes the \`drafted-by: ai\` line. A QA decision: an agent presents it and does not run it.`);
|
|
676
|
-
}
|
|
677
|
-
// create-test's checklist review of the viewpoint, still waiting for QA — advisory, never scored.
|
|
678
|
-
findings.push(...viewpointReviewOpenFindings(path.dirname(viewpointPath)));
|
|
679
|
-
if (adoptedPostHoc && !aiDraft) { // a draft's own finding already says this and names the accept command
|
|
668
|
+
if (adoptedPostHoc) {
|
|
680
669
|
findings.push(`VIEWPOINT-ADOPTED-POST-HOC: this is the first audit of \`requirements/test-viewpoint.md\` and the suite ALREADY has ${scenarios.length} scenarios, so the yardstick is being introduced against tests that already exist. A declaration written after the suite can only measure what its author already thought of — and if it was derived FROM the suite, \`atomicLedger\` and \`traceability\` read 100% by construction while anything dropped along the way stops being missing from anything. Both axes are held as unverified until you review the claims (add what you care about, delete what you do not) and confirm: \`sungen audit --screen ${screenName} --accept-viewpoint\` — a QA decision: an agent presents it and does not run it.`);
|
|
681
670
|
}
|
|
682
671
|
if (!hasYardstick) {
|
package/src/harness/next-step.ts
CHANGED
|
@@ -20,8 +20,6 @@ import { parse as parseYaml } from 'yaml';
|
|
|
20
20
|
import { reportSlug, featureFilesFor } from './unit-paths';
|
|
21
21
|
import { readTextFile } from './read-text';
|
|
22
22
|
import { checkAssetsDrift } from '../orchestrator/assets-drift';
|
|
23
|
-
import { viewpointFileIsAiDraft } from './viewpoint-draft-header';
|
|
24
|
-
import { readViewpointDeclaration } from './viewpoint-baseline';
|
|
25
23
|
|
|
26
24
|
export interface NextStep {
|
|
27
25
|
/** The command to run, as the operator would type it. */
|
|
@@ -43,8 +41,6 @@ export interface UnitState {
|
|
|
43
41
|
kind: 'screen' | 'flow' | 'api';
|
|
44
42
|
hasSpec: boolean;
|
|
45
43
|
hasViewpoint: boolean;
|
|
46
|
-
/** test-viewpoint.md carries `drafted-by: ai` — written by the AI, not yet accepted by a QA. */
|
|
47
|
-
viewpointIsAiDraft?: boolean;
|
|
48
44
|
hasContract: boolean;
|
|
49
45
|
hasFlowInventory: boolean;
|
|
50
46
|
scenarioCount: number;
|
|
@@ -161,9 +157,6 @@ export function readUnitState(
|
|
|
161
157
|
unit, kind,
|
|
162
158
|
hasSpec: exists(path.join(req, 'spec.md')),
|
|
163
159
|
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,
|
|
167
160
|
hasContract: exists(contractPath),
|
|
168
161
|
// A contract WITHOUT `flows:` can only be scored per phase, which is a materially weaker
|
|
169
162
|
// claim — so it is part of the state, not a detail.
|
|
@@ -252,15 +245,6 @@ export function deriveSteps(s: UnitState): NextStep[] {
|
|
|
252
245
|
});
|
|
253
246
|
return out;
|
|
254
247
|
}
|
|
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
|
-
}
|
|
264
248
|
if (s.scenarioCount === 0) {
|
|
265
249
|
out.push({ kind: 'do', command: design, because: 'no scenarios exist yet' });
|
|
266
250
|
return out;
|
|
@@ -276,7 +260,7 @@ export function deriveSteps(s: UnitState): NextStep[] {
|
|
|
276
260
|
out.push({ kind: 'do', command: `sungen audit --screen ${s.unit}`, because: 'the suite has never been measured' });
|
|
277
261
|
return out;
|
|
278
262
|
}
|
|
279
|
-
if (s.audit.viewpointBaselineStatus === 'changed'
|
|
263
|
+
if (s.audit.viewpointBaselineStatus === 'changed') {
|
|
280
264
|
out.push({
|
|
281
265
|
kind: 'decide',
|
|
282
266
|
command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
|
package/src/harness/parse.ts
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
10
|
import { readTextFile, readViewpointText } from './read-text';
|
|
11
|
-
import { parsePriority, toEntryPriority } from './viewpoint-priority';
|
|
12
11
|
import { featureFilesFor } from './unit-paths';
|
|
13
12
|
|
|
14
13
|
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
@@ -16,7 +15,7 @@ export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
|
16
15
|
export interface ViewpointEntry {
|
|
17
16
|
id: string; // e.g. VP-DATA-CONSISTENCY
|
|
18
17
|
priority: 'High' | 'Medium' | 'Low' | 'Unknown';
|
|
19
|
-
/** The declared priority word as written (`critical` · `high` · `medium` · `
|
|
18
|
+
/** The declared priority word as written (`critical` · `high` · `medium` · `normal` · `low` ·
|
|
20
19
|
* `deferred` · `skip`, which also stands for skipped / n/a / none / not applicable), kept because `priority` folds several of them together and the
|
|
21
20
|
* PRIORITY-CONTRADICTED check needs them apart. Absent when no priority was declared. */
|
|
22
21
|
priorityWord?: string;
|
|
@@ -26,14 +25,14 @@ export interface ViewpointEntry {
|
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Whole SECTIONS that declare something ABOUT the checklist rather than listing claims or
|
|
29
|
-
* viewpoints — known issues,
|
|
28
|
+
* viewpoints — known issues, traceability indexes, references, changelogs. Shared by the atomic
|
|
30
29
|
* ledger (their rows are not claims to prove, #630) and the viewpoint-overview parser (an id
|
|
31
30
|
* listed there is a reference, not a declaration, #833). `UI Patterns Identified` is the author's
|
|
32
31
|
* pick of WHICH sungen-viewpoint patterns apply — a `Skip` row there is not a claim to cover (#841).
|
|
33
32
|
* A `How to run` / `Procedure` section — that exact heading, optionally naming one id — is a manual
|
|
34
33
|
* check's steps, not claims of its own (#855); `How to test the login flow` stays a normal section.
|
|
35
34
|
*/
|
|
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?|
|
|
35
|
+
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?|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
|
|
37
36
|
|
|
38
37
|
/**
|
|
39
38
|
* Every word a viewpoint row may declare as its priority — ONE list for the overview parser, the
|
|
@@ -41,7 +40,7 @@ export const NON_DECLARATION_SECTION = /^(?:(?:ui patterns?(?: identified)?|(?:h
|
|
|
41
40
|
* `Skip` that the `sungen add` / `add-flow` scaffolds themselves suggest (#853). `deferred` and the
|
|
42
41
|
* not-applicable words are declarations as well: such a row is not a claim to cover.
|
|
43
42
|
*/
|
|
44
|
-
export const PRIORITY_WORDS = 'critical|high|medium|
|
|
43
|
+
export const PRIORITY_WORDS = 'critical|high|medium|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
|
|
45
44
|
/** "Declared not applicable" — folded to `skip` so every consumer checks one word. */
|
|
46
45
|
const NOT_APPLICABLE = /^(?:skip(?:ped)?|n\/a|none|not applicable)$/i;
|
|
47
46
|
|
|
@@ -55,7 +54,7 @@ const PRIORITY_HEADER = /^(?:priority|pri|level|severity|優先度?|độ ưu ti
|
|
|
55
54
|
function priorityOf(cell: string): { priority: ViewpointEntry['priority']; priorityWord?: string } {
|
|
56
55
|
const raw = cell.match(PRIORITY_WORD)?.[1].toLowerCase();
|
|
57
56
|
const word = raw && NOT_APPLICABLE.test(raw) ? 'skip' : raw;
|
|
58
|
-
const priority =
|
|
57
|
+
const priority = /high/i.test(cell) ? 'High' : /medium/i.test(cell) ? 'Medium' : /low/i.test(cell) ? 'Low' : 'Unknown';
|
|
59
58
|
return word ? { priority, priorityWord: word } : { priority };
|
|
60
59
|
}
|
|
61
60
|
|
|
@@ -64,9 +63,6 @@ export interface ScenarioInfo {
|
|
|
64
63
|
vpCode?: string; // VP-CART-001 style code in the scenario title
|
|
65
64
|
category?: string; // CART, LIST, VAL, ... (from the code)
|
|
66
65
|
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;
|
|
70
66
|
manual: boolean;
|
|
71
67
|
thenCount: number;
|
|
72
68
|
hasDataAssertion: boolean; // a Then asserts {{data}} / contains / match data
|
|
@@ -332,9 +328,6 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
332
328
|
const requiresCaps = tags.filter((t) => /^@requires:/i.test(t)).map((t) => t.slice('@requires:'.length).trim().toLowerCase()).filter(Boolean);
|
|
333
329
|
let priority: Priority = 'unknown';
|
|
334
330
|
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();
|
|
338
331
|
|
|
339
332
|
const { vpCode, category } = vpCodeOf(sc.name);
|
|
340
333
|
// Project-scheme ID: the leading token of the title (VP0-001 / MS-HP-001 / VP-LIST-001).
|
|
@@ -386,7 +379,6 @@ function classifyScenario(sc: ParsedScenario): ScenarioInfo {
|
|
|
386
379
|
vpCode,
|
|
387
380
|
category,
|
|
388
381
|
priority,
|
|
389
|
-
priorityTag,
|
|
390
382
|
manual,
|
|
391
383
|
thenCount,
|
|
392
384
|
hasDataAssertion: hasData,
|
package/src/harness/sensors.ts
CHANGED
|
@@ -12,7 +12,6 @@ import { parse as parseYaml } from 'yaml';
|
|
|
12
12
|
import { ScenarioInfo, ViewpointEntry, idPrefix } from './parse';
|
|
13
13
|
import { readTextFile } from './read-text';
|
|
14
14
|
import { keywordHit, hasKeyword } from './keyword-match';
|
|
15
|
-
import { parsePriority } from './viewpoint-priority';
|
|
16
15
|
import { countedAssertions } from './counted-assertions';
|
|
17
16
|
|
|
18
17
|
// Business-critical category keywords (matched by CONTAINMENT against the VP category, so a
|
|
@@ -1103,6 +1102,8 @@ export function priorityMismatches(
|
|
|
1103
1102
|
const declared = new Map<string, string>();
|
|
1104
1103
|
for (const v of viewpoints) if (v.priorityWord) declared.set(v.id.toUpperCase(), v.priorityWord);
|
|
1105
1104
|
if (declared.size === 0) return [];
|
|
1105
|
+
// `medium` and `normal` are the same rank under different names in real documents.
|
|
1106
|
+
const rank = (p: string): string => (p === 'medium' ? 'normal' : p);
|
|
1106
1107
|
const out: Array<{ id: string; declared: string; tagged: string; scenario: string }> = [];
|
|
1107
1108
|
for (const s of scenarios) {
|
|
1108
1109
|
const id = (s.vpId ?? '').toUpperCase();
|
|
@@ -1110,14 +1111,12 @@ export function priorityMismatches(
|
|
|
1110
1111
|
// The most specific declared row that prefixes this id.
|
|
1111
1112
|
const key = [...declared.keys()].filter((k) => id.startsWith(k)).sort((a, b) => b.length - a.length)[0];
|
|
1112
1113
|
if (!key) continue;
|
|
1113
|
-
|
|
1114
|
-
const
|
|
1115
|
-
// `deferred` and `skip` (declared not applicable, #853) have no tag equivalent —
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
if (!tagged) continue;
|
|
1120
|
-
if (want !== parsePriority(tagged)) out.push({ id: key, declared: declared.get(key)!, tagged, scenario: s.name });
|
|
1114
|
+
const want = rank(declared.get(key)!);
|
|
1115
|
+
const got = rank(String(s.priority).toLowerCase());
|
|
1116
|
+
// `critical`, `deferred` and `skip` (declared not applicable, #853) have no tag equivalent —
|
|
1117
|
+
// nothing to contradict.
|
|
1118
|
+
if (want === 'critical' || want === 'deferred' || want === 'skip') continue;
|
|
1119
|
+
if (want !== got) out.push({ id: key, declared: declared.get(key)!, tagged: got, scenario: s.name });
|
|
1121
1120
|
}
|
|
1122
1121
|
return out;
|
|
1123
1122
|
}
|
|
@@ -2,7 +2,6 @@ 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';
|
|
6
5
|
|
|
7
6
|
/**
|
|
8
7
|
* Keep the yardstick from moving with the thing it measures.
|
|
@@ -90,12 +89,7 @@ function fingerprint(ids: string[]): string {
|
|
|
90
89
|
*/
|
|
91
90
|
function fileFingerprint(viewpointPath: string): string {
|
|
92
91
|
try {
|
|
93
|
-
|
|
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);
|
|
92
|
+
return crypto.createHash('sha1').update(fs.readFileSync(viewpointPath)).digest('hex').slice(0, 12);
|
|
99
93
|
} catch { return ''; }
|
|
100
94
|
}
|
|
101
95
|
|