@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.
Files changed (177) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +3 -29
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/index.js +0 -2
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/exporters/test-data-resolver.d.ts.map +1 -1
  7. package/dist/exporters/test-data-resolver.js +12 -2
  8. package/dist/exporters/test-data-resolver.js.map +1 -1
  9. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  10. package/dist/generators/gherkin-parser/index.js +1 -0
  11. package/dist/generators/gherkin-parser/index.js.map +1 -1
  12. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  13. package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  14. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  15. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  16. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  17. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  18. package/dist/generators/test-generator/code-generator.js +4 -5
  19. package/dist/generators/test-generator/code-generator.js.map +1 -1
  20. package/dist/generators/test-generator/diagnostics.d.ts +9 -3
  21. package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
  22. package/dist/generators/test-generator/diagnostics.js +9 -3
  23. package/dist/generators/test-generator/diagnostics.js.map +1 -1
  24. package/dist/generators/test-generator/patterns/types.d.ts +1 -1
  25. package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
  26. package/dist/generators/test-generator/step-mapper.d.ts +8 -0
  27. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  28. package/dist/generators/test-generator/step-mapper.js +52 -8
  29. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  30. package/dist/generators/test-generator/utils/data-resolver.d.ts +16 -0
  31. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  32. package/dist/generators/test-generator/utils/data-resolver.js +59 -0
  33. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  34. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  35. package/dist/generators/test-generator/utils/selector-resolver.js +10 -2
  36. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  37. package/dist/harness/audit.d.ts.map +1 -1
  38. package/dist/harness/audit.js +2 -13
  39. package/dist/harness/audit.js.map +1 -1
  40. package/dist/harness/next-step.d.ts +0 -2
  41. package/dist/harness/next-step.d.ts.map +1 -1
  42. package/dist/harness/next-step.js +1 -15
  43. package/dist/harness/next-step.js.map +1 -1
  44. package/dist/harness/parse.d.ts +3 -6
  45. package/dist/harness/parse.d.ts.map +1 -1
  46. package/dist/harness/parse.js +4 -9
  47. package/dist/harness/parse.js.map +1 -1
  48. package/dist/harness/sensors.d.ts.map +1 -1
  49. package/dist/harness/sensors.js +9 -11
  50. package/dist/harness/sensors.js.map +1 -1
  51. package/dist/harness/viewpoint-baseline.d.ts.map +1 -1
  52. package/dist/harness/viewpoint-baseline.js +1 -7
  53. package/dist/harness/viewpoint-baseline.js.map +1 -1
  54. package/dist/harness/viewpoint-ledger.d.ts +3 -13
  55. package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
  56. package/dist/harness/viewpoint-ledger.js +8 -38
  57. package/dist/harness/viewpoint-ledger.js.map +1 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +2 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/orchestrator/ai-skills/commands.d.ts.map +1 -1
  62. package/dist/orchestrator/ai-skills/commands.js +5 -3
  63. package/dist/orchestrator/ai-skills/commands.js.map +1 -1
  64. package/dist/orchestrator/ai-skills/golden-skills.test.js +29 -0
  65. package/dist/orchestrator/ai-skills/golden-skills.test.js.map +1 -1
  66. package/dist/orchestrator/flow-manager.d.ts +1 -0
  67. package/dist/orchestrator/flow-manager.d.ts.map +1 -1
  68. package/dist/orchestrator/flow-manager.js +52 -3
  69. package/dist/orchestrator/flow-manager.js.map +1 -1
  70. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  71. package/dist/orchestrator/screen-manager.js +52 -4
  72. package/dist/orchestrator/screen-manager.js.map +1 -1
  73. package/dist/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  74. package/dist/orchestrator/templates/ai-src/commands/create-test.md +22 -30
  75. package/dist/orchestrator/templates/ai-src/config/claude.md +20 -7
  76. package/dist/orchestrator/templates/ai-src/config/copilot.md +20 -7
  77. package/dist/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  78. package/dist/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  79. package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  80. package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  81. package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
  82. package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
  83. package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
  84. package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
  85. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
  86. package/dist/orchestrator/templates/specs-test-data.ts +22 -4
  87. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts +1 -4
  88. package/dist/orchestrator/viewpoint-pattern-catalog.d.ts.map +1 -1
  89. package/dist/orchestrator/viewpoint-pattern-catalog.js +2 -5
  90. package/dist/orchestrator/viewpoint-pattern-catalog.js.map +1 -1
  91. package/package.json +3 -3
  92. package/src/cli/commands/audit.ts +3 -26
  93. package/src/cli/index.ts +0 -2
  94. package/src/exporters/test-data-resolver.ts +10 -2
  95. package/src/generators/gherkin-parser/index.ts +1 -0
  96. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/contain-text-assertion.hbs +2 -1
  97. package/src/generators/test-generator/adapters/appium/templates/steps/assertions/expanded-assertion.hbs +2 -0
  98. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/date-picker-unsupported.hbs +5 -0
  99. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/fill-action.hbs +13 -1
  100. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/contain-text-assertion.hbs +9 -1
  101. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/expanded-assertion.hbs +12 -0
  102. package/src/generators/test-generator/code-generator.ts +4 -4
  103. package/src/generators/test-generator/diagnostics.ts +9 -3
  104. package/src/generators/test-generator/patterns/types.ts +2 -0
  105. package/src/generators/test-generator/step-mapper.ts +56 -9
  106. package/src/generators/test-generator/utils/data-resolver.ts +53 -0
  107. package/src/generators/test-generator/utils/selector-resolver.ts +10 -2
  108. package/src/harness/audit.ts +2 -13
  109. package/src/harness/next-step.ts +1 -17
  110. package/src/harness/parse.ts +5 -13
  111. package/src/harness/sensors.ts +8 -9
  112. package/src/harness/viewpoint-baseline.ts +1 -7
  113. package/src/harness/viewpoint-ledger.ts +11 -52
  114. package/src/index.ts +2 -1
  115. package/src/orchestrator/ai-skills/commands.ts +5 -3
  116. package/src/orchestrator/ai-skills/golden-skills.test.ts +23 -0
  117. package/src/orchestrator/flow-manager.ts +53 -3
  118. package/src/orchestrator/screen-manager.ts +52 -4
  119. package/src/orchestrator/templates/ai-src/commands/add-flow.md +26 -4
  120. package/src/orchestrator/templates/ai-src/commands/create-test.md +22 -30
  121. package/src/orchestrator/templates/ai-src/config/claude.md +20 -7
  122. package/src/orchestrator/templates/ai-src/config/copilot.md +20 -7
  123. package/src/orchestrator/templates/ai-src/skills/sungen-api-coverage-model/SKILL.md +1 -1
  124. package/src/orchestrator/templates/ai-src/skills/sungen-capture-mobile/SKILL.md +3 -3
  125. package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +9 -0
  126. package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -1
  127. package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +5 -4
  128. package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +24 -8
  129. package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -3
  130. package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +5 -4
  131. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +0 -18
  132. package/src/orchestrator/templates/specs-test-data.ts +22 -4
  133. package/src/orchestrator/viewpoint-pattern-catalog.ts +2 -5
  134. package/dist/cli/commands/viewpoint.d.ts +0 -7
  135. package/dist/cli/commands/viewpoint.d.ts.map +0 -1
  136. package/dist/cli/commands/viewpoint.js +0 -70
  137. package/dist/cli/commands/viewpoint.js.map +0 -1
  138. package/dist/harness/viewpoint-draft-header.d.ts +0 -7
  139. package/dist/harness/viewpoint-draft-header.d.ts.map +0 -1
  140. package/dist/harness/viewpoint-draft-header.js +0 -100
  141. package/dist/harness/viewpoint-draft-header.js.map +0 -1
  142. package/dist/harness/viewpoint-mode.d.ts +0 -9
  143. package/dist/harness/viewpoint-mode.d.ts.map +0 -1
  144. package/dist/harness/viewpoint-mode.js +0 -52
  145. package/dist/harness/viewpoint-mode.js.map +0 -1
  146. package/dist/harness/viewpoint-priority.d.ts +0 -19
  147. package/dist/harness/viewpoint-priority.d.ts.map +0 -1
  148. package/dist/harness/viewpoint-priority.js +0 -36
  149. package/dist/harness/viewpoint-priority.js.map +0 -1
  150. package/dist/harness/viewpoint-review-open.d.ts +0 -6
  151. package/dist/harness/viewpoint-review-open.d.ts.map +0 -1
  152. package/dist/harness/viewpoint-review-open.js +0 -90
  153. package/dist/harness/viewpoint-review-open.js.map +0 -1
  154. package/dist/harness/viewpoint-scan-refs.d.ts +0 -50
  155. package/dist/harness/viewpoint-scan-refs.d.ts.map +0 -1
  156. package/dist/harness/viewpoint-scan-refs.js +0 -200
  157. package/dist/harness/viewpoint-scan-refs.js.map +0 -1
  158. package/dist/harness/viewpoint-scan.d.ts +0 -41
  159. package/dist/harness/viewpoint-scan.d.ts.map +0 -1
  160. package/dist/harness/viewpoint-scan.js +0 -118
  161. package/dist/harness/viewpoint-scan.js.map +0 -1
  162. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
  163. package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
  164. package/dist/orchestrator/viewpoint-scaffold.d.ts +0 -3
  165. package/dist/orchestrator/viewpoint-scaffold.d.ts.map +0 -1
  166. package/dist/orchestrator/viewpoint-scaffold.js +0 -70
  167. package/dist/orchestrator/viewpoint-scaffold.js.map +0 -1
  168. package/src/cli/commands/viewpoint.ts +0 -58
  169. package/src/harness/viewpoint-draft-header.ts +0 -55
  170. package/src/harness/viewpoint-mode.ts +0 -49
  171. package/src/harness/viewpoint-priority.ts +0 -38
  172. package/src/harness/viewpoint-review-open.ts +0 -51
  173. package/src/harness/viewpoint-scan-refs.ts +0 -154
  174. package/src/harness/viewpoint-scan.ts +0 -111
  175. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/review-checklist.md +0 -63
  176. package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/viewpoint-template.md +0 -158
  177. package/src/orchestrator/viewpoint-scaffold.ts +0 -36
@@ -1 +1,13 @@
1
- await {{> locator}}.fill('{{escapeQuotes fillValue}}');
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
- await expect({{> locator}}).toContainText('{{escapeQuotes expectedText}}');
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* that
1191
- // exist only at runtime — register them as captured so they resolve to a runtime
1192
- // get() (→ rowData.get) instead of compile-time YAML lookup.
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
- for (const r of refs) this.stepMapper.registerCaptured(r);
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: this compiles
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). The mirror of SG-W004 — that one reports a field
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
@@ -38,6 +38,8 @@ export type StateClause =
38
38
  | 'empty'
39
39
  | 'loading'
40
40
  | 'selected'
41
+ | 'expanded'
42
+ | 'collapsed'
41
43
  | 'sorted ascending'
42
44
  | 'sorted descending';
43
45
 
@@ -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 { resolvedValue = this.dataResolver.peekBaseValue(step.dataRef, this.featureName); } catch { /* unresolved at compile time → cannot judge */ }
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
- const value = this.dataResolver.peekBaseValue(step.dataRef, this.featureName);
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
- if (!hit) return null;
860
-
861
- const reason = `step "${step.text}" carries the clause \`${hit[1]}\` but compiled to a bare `
862
- + `visibility check — the clause was dropped, so the assertion would pass whatever the element `
863
- + `says. Either the phrasing is not the supported one for that clause (see the `
864
- + `sungen-gherkin-syntax reference), or this sungen predates the step: check you are running `
865
- + `the project-local compiler (\`npx sungen\`), not an older global one.`;
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' and drops `exact` — putting `upload` in the switch left the two spellings
1028
- // diverging for any entry that sets `locator:`. `withExtras` still stamps the declared
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':
@@ -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
- // An AI draft rides the same hold: its line is only removed when a human accepts it.
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 (aiDraft) {
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) {
@@ -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' && !s.viewpointIsAiDraft) { // the draft's own decide step is already above
263
+ if (s.audit.viewpointBaselineStatus === 'changed') {
280
264
  out.push({
281
265
  kind: 'decide',
282
266
  command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
@@ -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` · `med` · `normal` · `low` ·
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, spec gaps, traceability indexes, references and appendices, changelogs. Shared by the atomic
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?|spec gaps?|appendi(?:x|ces)|phụ lục|deferred|out of scope|viewpoint grouping|traceability|metadata|scope|journey phases?|revision history|references?|sources?|changelog)\b)/i;
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|med|normal|low|deferred|skip(?:ped)?|n\\/a|none|not applicable';
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 = toEntryPriority(parsePriority(cell));
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,
@@ -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
- // Both sides fold through the one priority reading (`critical` ≡ high, `medium`/`med` ≡ normal).
1114
- const want = parsePriority(declared.get(key)!);
1115
- // `deferred` and `skip` (declared not applicable, #853) have no tag equivalent — nothing to contradict.
1116
- if (want === 'deferred' || want === 'skip') continue;
1117
- // An untagged scenario states no priority, so it cannot contradict one (#861).
1118
- const tagged = s.priorityTag ?? (s.priority === 'unknown' ? undefined : String(s.priority));
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
- 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);
92
+ return crypto.createHash('sha1').update(fs.readFileSync(viewpointPath)).digest('hex').slice(0, 12);
99
93
  } catch { return ''; }
100
94
  }
101
95