@sun-asterisk/sungen 3.2.17 → 3.2.18
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 +23 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +209 -16
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/index.js +5 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +8 -1
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/api-environment.d.ts +11 -0
- package/dist/exporters/api-environment.d.ts.map +1 -0
- package/dist/exporters/api-environment.js +129 -0
- package/dist/exporters/api-environment.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts +16 -1
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +21 -2
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts +70 -0
- package/dist/exporters/matrix/build.d.ts.map +1 -0
- package/dist/exporters/matrix/build.js +516 -0
- package/dist/exporters/matrix/build.js.map +1 -0
- package/dist/exporters/matrix/export.d.ts +50 -0
- package/dist/exporters/matrix/export.d.ts.map +1 -0
- package/dist/exporters/matrix/export.js +152 -0
- package/dist/exporters/matrix/export.js.map +1 -0
- package/dist/exporters/matrix/fingerprint.d.ts +20 -0
- package/dist/exporters/matrix/fingerprint.d.ts.map +1 -0
- package/dist/exporters/matrix/fingerprint.js +83 -0
- package/dist/exporters/matrix/fingerprint.js.map +1 -0
- package/dist/exporters/matrix/gates.d.ts +21 -0
- package/dist/exporters/matrix/gates.d.ts.map +1 -0
- package/dist/exporters/matrix/gates.js +367 -0
- package/dist/exporters/matrix/gates.js.map +1 -0
- package/dist/exporters/matrix/map-loader.d.ts +22 -0
- package/dist/exporters/matrix/map-loader.d.ts.map +1 -0
- package/dist/exporters/matrix/map-loader.js +193 -0
- package/dist/exporters/matrix/map-loader.js.map +1 -0
- package/dist/exporters/matrix/render-csv.d.ts +9 -0
- package/dist/exporters/matrix/render-csv.d.ts.map +1 -0
- package/dist/exporters/matrix/render-csv.js +92 -0
- package/dist/exporters/matrix/render-csv.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +41 -0
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -0
- package/dist/exporters/matrix/render-xlsx.js +430 -0
- package/dist/exporters/matrix/render-xlsx.js.map +1 -0
- package/dist/exporters/matrix/types.d.ts +199 -0
- package/dist/exporters/matrix/types.d.ts.map +1 -0
- package/dist/exporters/matrix/types.js +15 -0
- package/dist/exporters/matrix/types.js.map +1 -0
- package/dist/exporters/matrix/wording.d.ts +61 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -0
- package/dist/exporters/matrix/wording.js +221 -0
- package/dist/exporters/matrix/wording.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +2 -1
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +2 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts +1 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +3 -3
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/harness/audit.d.ts +10 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +98 -16
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/blindspot.d.ts.map +1 -1
- package/dist/harness/blindspot.js +2 -1
- package/dist/harness/blindspot.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +2 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +17 -4
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/feedback.d.ts.map +1 -1
- package/dist/harness/feedback.js +3 -2
- package/dist/harness/feedback.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +2 -1
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +3 -2
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/intent.d.ts.map +1 -1
- package/dist/harness/intent.js +2 -1
- package/dist/harness/intent.js.map +1 -1
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +3 -2
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/ledger.d.ts.map +1 -1
- package/dist/harness/ledger.js +3 -2
- package/dist/harness/ledger.js.map +1 -1
- package/dist/harness/manifest.d.ts.map +1 -1
- package/dist/harness/manifest.js +4 -3
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +16 -3
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/quality-gates.d.ts.map +1 -1
- package/dist/harness/quality-gates.js +2 -1
- package/dist/harness/quality-gates.js.map +1 -1
- package/dist/harness/read-text.d.ts +22 -0
- package/dist/harness/read-text.d.ts.map +1 -0
- package/dist/harness/read-text.js +64 -0
- package/dist/harness/read-text.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +3 -2
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/sensors.d.ts +13 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +63 -20
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/spec-coverage.d.ts +5 -0
- package/dist/harness/spec-coverage.d.ts.map +1 -1
- package/dist/harness/spec-coverage.js +17 -7
- package/dist/harness/spec-coverage.js.map +1 -1
- package/dist/harness/trace.d.ts.map +1 -1
- package/dist/harness/trace.js +4 -3
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/viewpoint-baseline.d.ts +49 -0
- package/dist/harness/viewpoint-baseline.d.ts.map +1 -0
- package/dist/harness/viewpoint-baseline.js +141 -0
- package/dist/harness/viewpoint-baseline.js.map +1 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +2 -1
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts +24 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -0
- package/dist/orchestrator/assets-drift.js +80 -0
- package/dist/orchestrator/assets-drift.js.map +1 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +24 -0
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +203 -120
- package/dist/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +23 -1
- package/src/cli/commands/delivery.ts +240 -18
- package/src/cli/index.ts +6 -0
- package/src/dashboard/snapshot-builder.ts +8 -1
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/api-environment.ts +97 -0
- package/src/exporters/feature-parser.ts +21 -2
- package/src/exporters/matrix/build.ts +551 -0
- package/src/exporters/matrix/export.ts +149 -0
- package/src/exporters/matrix/fingerprint.ts +49 -0
- package/src/exporters/matrix/gates.ts +400 -0
- package/src/exporters/matrix/map-loader.ts +163 -0
- package/src/exporters/matrix/render-csv.ts +94 -0
- package/src/exporters/matrix/render-xlsx.ts +420 -0
- package/src/exporters/matrix/types.ts +226 -0
- package/src/exporters/matrix/wording.ts +221 -0
- package/src/exporters/scenario-merger.ts +2 -1
- package/src/exporters/spec-parser.ts +2 -1
- package/src/exporters/xlsx-report-builder.ts +3 -2
- package/src/harness/audit.ts +104 -18
- package/src/harness/blindspot.ts +2 -1
- package/src/harness/capability-plan.ts +17 -4
- package/src/harness/feedback.ts +3 -2
- package/src/harness/flow-check.ts +2 -1
- package/src/harness/flow-plan.ts +3 -2
- package/src/harness/intent.ts +2 -1
- package/src/harness/journey.ts +3 -2
- package/src/harness/ledger.ts +3 -2
- package/src/harness/manifest.ts +4 -3
- package/src/harness/parse.ts +17 -3
- package/src/harness/quality-gates.ts +2 -1
- package/src/harness/read-text.ts +28 -0
- package/src/harness/script-check.ts +3 -2
- package/src/harness/sensors.ts +55 -8
- package/src/harness/spec-coverage.ts +22 -7
- package/src/harness/trace.ts +4 -3
- package/src/harness/viewpoint-baseline.ts +128 -0
- package/src/harness/viewpoint-ledger.ts +2 -1
- package/src/orchestrator/assets-drift.ts +64 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +24 -0
- package/src/orchestrator/templates/ai-src/commands/delivery.md +203 -120
- package/src/orchestrator/templates/ai-src/config/claude.md +1 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +102 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +23 -0
package/src/harness/audit.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { loadScenarios, parseViewpointOverview, ScenarioInfo, ViewpointEntry } from './parse';
|
|
12
|
+
import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline';
|
|
12
13
|
import { featureBasename } from './unit-paths';
|
|
13
14
|
import {
|
|
14
15
|
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
@@ -27,6 +28,7 @@ import { viewpointLedger, parseViewpointItems, LedgerResult } from './viewpoint-
|
|
|
27
28
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
28
29
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
29
30
|
import { contextRouter } from '../capabilities/context-router';
|
|
31
|
+
import { readTextFile } from './read-text';
|
|
30
32
|
|
|
31
33
|
export interface AuditReport {
|
|
32
34
|
screen: string;
|
|
@@ -45,13 +47,20 @@ export interface AuditReport {
|
|
|
45
47
|
flowDepth: FlowDepthResult; // H3 — stateful-flow regression depth (count / teardown / multi-source)
|
|
46
48
|
oracle: OracleStrengthResult; // H4 — facet-oracle strength (weak name-substring vs title/detail/API/DB)
|
|
47
49
|
ledger: LedgerResult; // atomic viewpoint-item coverage (per-bullet status)
|
|
50
|
+
viewpointBaseline: ViewpointBaseline; // is the yardstick still the accepted one? (#557)
|
|
48
51
|
calibration: { // #8 — multi-axis score so a high overall can't hide a weak axis
|
|
49
52
|
axes: Record<string, number>;
|
|
50
53
|
weakest: { axis: string; value: number };
|
|
51
54
|
inflated: boolean;
|
|
55
|
+
/** Axes excluded for lack of evidence (no spec / viewpoint file / decidable page type). */
|
|
56
|
+
notApplicable?: string[];
|
|
57
|
+
/** Set when a weak critical axis holds the overall down. */
|
|
58
|
+
cappedAt?: number;
|
|
52
59
|
};
|
|
53
60
|
score: {
|
|
54
|
-
overall: number; // 0..10, business-weighted
|
|
61
|
+
overall: number; // 0..10, business-weighted over the APPLICABLE axes
|
|
62
|
+
notApplicable?: string[]; // axes with no evidence — excluded, not scored as 1.0
|
|
63
|
+
cappedAt?: number; // set when a weak critical axis holds the score down
|
|
55
64
|
coverage: number; // 0..1
|
|
56
65
|
businessDepth: number; // 0..1
|
|
57
66
|
balance: number; // 0..1
|
|
@@ -66,7 +75,7 @@ export interface AuditReport {
|
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
/** The catalog-resolution id for a unit dir (relative to qa/): screen · flows/<flow> · api/<area> · api/flows/<flow>. */
|
|
69
|
-
function catalogIdFromScreenDir(screenDir: string): string {
|
|
78
|
+
export function catalogIdFromScreenDir(screenDir: string): string {
|
|
70
79
|
const parts = screenDir.split(path.sep);
|
|
71
80
|
const qa = parts.lastIndexOf('qa');
|
|
72
81
|
if (qa >= 0) {
|
|
@@ -118,7 +127,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
118
127
|
const catalogScreenName = catalogIdFromScreenDir(screenDir);
|
|
119
128
|
|
|
120
129
|
const specPath = path.join(screenDir, 'requirements', 'spec.md');
|
|
121
|
-
const featureText = fs.existsSync(featurePath) ?
|
|
130
|
+
const featureText = fs.existsSync(featurePath) ? readTextFile(featurePath) : '';
|
|
122
131
|
|
|
123
132
|
const scenarios: ScenarioInfo[] = loadScenarios(featurePath);
|
|
124
133
|
const viewpoints: ViewpointEntry[] = parseViewpointOverview(viewpointPath);
|
|
@@ -147,7 +156,11 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
147
156
|
const capGate = scoringCap?.gateProvider as
|
|
148
157
|
((i: { scenarios: ScenarioInfo[]; viewpoints: ViewpointEntry[]; catalog: Catalog; focus: typeof intent.focus; cwd: string; screenName: string; threshold: number; businessCriticalMethods?: string[] }) => { gate: GateResult; depth: DepthResult }) | undefined;
|
|
149
158
|
const provided = capGate?.({ scenarios, viewpoints, catalog, focus: intent.focus, cwd: projectRootFromScreenDir(screenDir), screenName: catalogScreenName, threshold: depthThresholdFor(intent.focus), businessCriticalMethods: intent.businessCriticalMethods });
|
|
150
|
-
|
|
159
|
+
// Is the declaration this suite is measured against still the one a human accepted?
|
|
160
|
+
const viewpointBaseline = checkViewpointBaseline(
|
|
161
|
+
projectRootFromScreenDir(screenDir), catalogScreenName, viewpointPath);
|
|
162
|
+
const viewpointText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
|
|
163
|
+
const gate = provided?.gate ?? viewpointGate(scenarios, viewpoints, catalog, platform === 'mobile', viewpointText);
|
|
151
164
|
const depth = provided?.depth ?? assertionDepth(scenarios, dataThemesFor(catalog, gate.pageType), intent.focus);
|
|
152
165
|
|
|
153
166
|
// TQ-4 — deferral-aware coverage credit: an inherently cross-screen theme (cart / detail /
|
|
@@ -211,8 +224,57 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
211
224
|
: 1;
|
|
212
225
|
const traceScore = 0.5 * trace.withVpCodeRatio + 0.5 * trace.mappedRatio;
|
|
213
226
|
|
|
214
|
-
// Business-weighted overall
|
|
215
|
-
|
|
227
|
+
// ---- Business-weighted overall -------------------------------------------
|
|
228
|
+
//
|
|
229
|
+
// Two rules make the number mean what it says:
|
|
230
|
+
//
|
|
231
|
+
// 1. Only axes the project supplies EVIDENCE for are scored, and the weights
|
|
232
|
+
// are renormalised over them. The old formula spent 40% on page-type theme
|
|
233
|
+
// coverage — which is 1.0 whenever no page type is detected — so a screen
|
|
234
|
+
// with no detectable type collected 4 free points, while a mis-detected one
|
|
235
|
+
// was judged against themes it can never have. Missing spec / viewpoint
|
|
236
|
+
// files are treated the same way: absent evidence scores nothing, in either
|
|
237
|
+
// direction.
|
|
238
|
+
// 2. The three axes the harness already measured but never counted —
|
|
239
|
+
// claimProof, specFR, atomicLedger — now carry weight. A suite where two
|
|
240
|
+
// thirds of the scenarios do not prove their own title used to score 7.9.
|
|
241
|
+
const specRatio = spec.frTotal ? spec.frCovered / spec.frTotal : 1;
|
|
242
|
+
// 3. Evidence has to be INDEPENDENT of what it judges. `atomicLedger` and
|
|
243
|
+
// `traceability` both measure the suite against test-viewpoint.md, so a run
|
|
244
|
+
// that rewrites that file scores them 100% by construction — which is exactly
|
|
245
|
+
// what a create-test run did, silently dropping the performance viewpoint on
|
|
246
|
+
// the way. While the declaration is unconfirmed, neither axis is evidence.
|
|
247
|
+
const viewpointMoved = viewpointBaseline.status === 'changed';
|
|
248
|
+
const axisDefs: Array<{ key: string; value: number; weight: number; applicable: boolean; critical: boolean }> = [
|
|
249
|
+
{ key: 'coverage', value: coverage, weight: 0.22, applicable: !!gate.pageType && gate.themesTotal > 0, critical: true },
|
|
250
|
+
{ key: 'specFR', value: specRatio, weight: 0.15, applicable: spec.hasSpec && spec.frTotal > 0, critical: true },
|
|
251
|
+
{ key: 'atomicLedger', value: ledger.ratio, weight: 0.13, applicable: ledger.hasViewpoint && ledger.total > 0 && !viewpointMoved, critical: true },
|
|
252
|
+
{ key: 'businessDepth', value: businessDepth, weight: 0.20, applicable: true, critical: true },
|
|
253
|
+
{ key: 'claimProof', value: claim.ratio, weight: 0.15, applicable: claim.withClaims > 0, critical: true },
|
|
254
|
+
{ key: 'traceability', value: traceScore, weight: 0.09, applicable: viewpoints.length > 0 && !viewpointMoved, critical: false },
|
|
255
|
+
{ key: 'balance', value: balanceScore, weight: 0.06, applicable: true, critical: false },
|
|
256
|
+
];
|
|
257
|
+
const scored = axisDefs.filter((a) => a.applicable);
|
|
258
|
+
const weightSum = scored.reduce((t, a) => t + a.weight, 0) || 1;
|
|
259
|
+
const weighted = scored.reduce((t, a) => t + a.weight * a.value, 0) / weightSum;
|
|
260
|
+
|
|
261
|
+
// A weak axis is a hole, and a hole must not be averaged away: the pass
|
|
262
|
+
// threshold teams set (7.x) has to mean "no hole", not "good on balance".
|
|
263
|
+
const criticalScored = scored.filter((a) => a.critical);
|
|
264
|
+
const weakestCritical = criticalScored.slice().sort((a, b) => a.value - b.value)[0];
|
|
265
|
+
const CAP_SEVERE = 6.9; // below any 7.x gate
|
|
266
|
+
const CAP_WEAK = 7.9;
|
|
267
|
+
// Dropping an axis for lack of evidence must not become a shortcut to a perfect
|
|
268
|
+
// score: with no spec and no viewpoint file the remaining axes are easy to max.
|
|
269
|
+
// A top mark has to rest on complete evidence, so partial evidence caps below it.
|
|
270
|
+
const CAP_PARTIAL_EVIDENCE = 8.9;
|
|
271
|
+
let cap = 10;
|
|
272
|
+
if (weakestCritical && weakestCritical.value < 0.5) cap = CAP_SEVERE;
|
|
273
|
+
else if (weakestCritical && weakestCritical.value < 0.7) cap = CAP_WEAK;
|
|
274
|
+
const missingEvidence = axisDefs.filter((a) => !a.applicable).map((a) => a.key);
|
|
275
|
+
if (missingEvidence.length > 0) cap = Math.min(cap, CAP_PARTIAL_EVIDENCE);
|
|
276
|
+
const rawOverall = Math.min(weighted * 10, cap);
|
|
277
|
+
const capped = weakestCritical ? weighted * 10 > cap : false;
|
|
216
278
|
// H7 — senior-grade band: the top decile (≥9) is reserved for suites that also clear the senior
|
|
217
279
|
// axes — a stateful flow with FULL regression depth (count + teardown + multi-source), no weak
|
|
218
280
|
// facet oracle, and no parallel-cart isolation risk. Otherwise the score is held just below 9, so
|
|
@@ -221,11 +283,27 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
221
283
|
const overall = seniorBandedOverall(rawOverall, { flowStateful: flowDepth.stateful, flowRatio: flowDepth.ratio, oracleWeak: oracle.weak.length, isolationRisk: isoRisk });
|
|
222
284
|
|
|
223
285
|
const findings: string[] = [];
|
|
286
|
+
if (capped) {
|
|
287
|
+
const why = weakestCritical && weakestCritical.value < 0.7
|
|
288
|
+
? `"${weakestCritical.key}" is ${(weakestCritical.value * 100).toFixed(0)}% — a weak axis is a hole, not something the other axes can average away`
|
|
289
|
+
: `this unit supplies no evidence for [${missingEvidence.join(', ')}], and a top mark has to rest on complete evidence`;
|
|
290
|
+
findings.push(`SCORE-CAPPED: overall held at ${cap.toFixed(1)} because ${why}.`);
|
|
291
|
+
}
|
|
292
|
+
if (viewpointBaseline.status === 'changed') {
|
|
293
|
+
const moved = [
|
|
294
|
+
viewpointBaseline.removed?.length ? `removed [${viewpointBaseline.removed.join(', ')}]` : '',
|
|
295
|
+
viewpointBaseline.added?.length ? `added [${viewpointBaseline.added.join(', ')}]` : '',
|
|
296
|
+
].filter(Boolean).join(', ');
|
|
297
|
+
findings.push(`VIEWPOINT-BASELINE-CHANGED: test-viewpoint.md no longer declares what it did when this unit was last accepted — ${moved || 'the declared ids were reordered or replaced'}. atomicLedger + traceability measure the suite AGAINST this file, so they are excluded from the score until the change is confirmed: a generator that rewrites the declaration scores both 100% by construction, and a viewpoint dropped from the file stops being missing from anything. Review the diff (a removed id means that coverage is now unclaimed), then run \`sungen audit --screen ${screenName} --accept-viewpoint\`.`);
|
|
298
|
+
}
|
|
299
|
+
if (gate.pageTypeSource === 'undetermined') {
|
|
300
|
+
findings.push(`PAGE-TYPE-UNDETERMINED: no page type fits this screen with enough confidence (best ${gate.pageTypeEvidence?.hits ?? 0} keyword hit(s)), so NO critical themes were demanded and the coverage axis is excluded from the score — declare it in test-viewpoint.md (\`page-type: <id>\`) to have the theme checklist applied.`);
|
|
301
|
+
}
|
|
224
302
|
for (const c of flowCredits) {
|
|
225
303
|
findings.push(`COVERED-VIA-FLOW: critical theme "${c.theme}" is not covered on this screen but is deeply covered by flow "${c.flow}" — cross-screen depth correctly owned by the flow, so it is credited (not a screen gap). Verify with \`sungen flow-check\`.`);
|
|
226
304
|
}
|
|
227
305
|
// Verification-scope (advisory): API/DB verification the project's recorded test type never asked for.
|
|
228
|
-
for (const v of verificationScopeFindings(featureText, fs.existsSync(viewpointPath) ?
|
|
306
|
+
for (const v of verificationScopeFindings(featureText, fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '', capProfile)) {
|
|
229
307
|
findings.push(`VERIFICATION-OUT-OF-SCOPE (${v.layer}): this unit ${v.message}`);
|
|
230
308
|
}
|
|
231
309
|
for (const g of gate.gaps) {
|
|
@@ -289,6 +367,14 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
289
367
|
for (const u of spec.uncoveredMust) {
|
|
290
368
|
findings.push(`SPEC-UNCOVERED: ${u.id} (MUST) has no covering scenario — "${u.text}" → add a scenario or tag one @spec:${u.id}.`);
|
|
291
369
|
}
|
|
370
|
+
if (spec.inferredOnly.length > 0) {
|
|
371
|
+
// Covered, but only by keyword inference — no scenario cites the id. Delivery's
|
|
372
|
+
// requirement table (and any later refactor) cannot follow an inferred link, so
|
|
373
|
+
// the trace has to be written down while the author still knows which scenario
|
|
374
|
+
// proves what.
|
|
375
|
+
const shown = spec.inferredOnly.slice(0, 8).join(', ');
|
|
376
|
+
findings.push(`SPEC-TRACE-IMPLICIT: ${spec.inferredOnly.length} requirement(s) are covered by keyword inference only (${shown}${spec.inferredOnly.length > 8 ? ', …' : ''}) — add \`@spec:<id>\` to the scenario that proves each so the trace survives edits and delivery can report it.`);
|
|
377
|
+
}
|
|
292
378
|
if (taxonomyMismatch) {
|
|
293
379
|
findings.push(`VP-TAXONOMY-MISMATCH: only ${(trace.mappedRatio * 100).toFixed(0)}% of scenarios use the viewpoint IDs declared in test-viewpoint.md — scenarios invented a generic VP-<CAT> scheme. Re-tag to the project's viewpoint IDs so the coverage matrix is accurate.`);
|
|
294
380
|
}
|
|
@@ -367,15 +453,15 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
367
453
|
// #8 — multi-axis calibration: a high overall must not hide a weak axis.
|
|
368
454
|
const manualCompleteness = manualOracleResult.manualTotal
|
|
369
455
|
? 1 - manualOracleResult.insufficient.length / manualOracleResult.manualTotal : 1;
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
456
|
+
// Only the axes that were actually scored appear here — an axis with no evidence
|
|
457
|
+
// (no spec, no viewpoint file, no decidable page type) is reported as such rather
|
|
458
|
+
// than as a silent 100%, which is what let empty inputs look like perfect ones.
|
|
459
|
+
const axes: Record<string, number> = {};
|
|
460
|
+
for (const a of scored) axes[a.key] = Math.round(a.value * 100) / 100;
|
|
461
|
+
// Measured but not weighted: they drive findings, not the headline.
|
|
462
|
+
axes.manualOracle = Math.round(manualCompleteness * 100) / 100;
|
|
463
|
+
axes.taxonomy = taxonomyMismatch ? 0 : Math.round(trace.mappedRatio * 100) / 100;
|
|
464
|
+
const notApplicable = axisDefs.filter((a) => !a.applicable).map((a) => a.key);
|
|
379
465
|
const weakestEntry = Object.entries(axes).sort((a, b) => a[1] - b[1])[0];
|
|
380
466
|
const weakest = { axis: weakestEntry[0], value: weakestEntry[1] };
|
|
381
467
|
// Inflated when breadth hides a weak axis, OR when businessDepth is high but rests on a
|
|
@@ -386,7 +472,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
386
472
|
if (inflated) {
|
|
387
473
|
findings.push(`SCORE-INFLATED-BY-BREADTH: overall ${Math.round(overall * 10) / 10}/10 but the weakest axis "${weakest.axis}" is ${(weakest.value * 100).toFixed(0)}% — breadth is hiding a weak dimension. Raise "${weakest.axis}" before trusting the headline.`);
|
|
388
474
|
}
|
|
389
|
-
const calibration = { axes, weakest, inflated };
|
|
475
|
+
const calibration = { axes, weakest, inflated, notApplicable, cappedAt: capped ? cap : undefined };
|
|
390
476
|
|
|
391
477
|
// Gate spans coverage (viewpoint themes), depth, claim-proof, spec-clause coverage,
|
|
392
478
|
// AND taxonomy-match (scenarios must use the project's viewpoint IDs when defined).
|
|
@@ -397,7 +483,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
397
483
|
screen: screenName,
|
|
398
484
|
scenarioCount: scenarios.length,
|
|
399
485
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
400
|
-
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, calibration,
|
|
486
|
+
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
401
487
|
score: {
|
|
402
488
|
overall: Math.round(overall * 10) / 10,
|
|
403
489
|
coverage: Math.round(coverage * 100) / 100,
|
package/src/harness/blindspot.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
+
import { readTextFile } from './read-text';
|
|
13
14
|
|
|
14
15
|
export interface Blindspot {
|
|
15
16
|
ts: string;
|
|
@@ -35,7 +36,7 @@ export function addBlindspot(entry: Omit<Blindspot, 'ts'> & { ts?: string }): st
|
|
|
35
36
|
export function listBlindspots(): Blindspot[] {
|
|
36
37
|
const p = storePath();
|
|
37
38
|
if (!fs.existsSync(p)) return [];
|
|
38
|
-
return
|
|
39
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => {
|
|
39
40
|
try { return JSON.parse(l) as Blindspot; } catch { return null; }
|
|
40
41
|
}).filter(Boolean) as Blindspot[];
|
|
41
42
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import { loadDriverCatalog } from './capability';
|
|
12
|
+
import { readTextFile } from './read-text';
|
|
12
13
|
|
|
13
14
|
export type ReasonClass = 'capability' | 'keep' | 'flow';
|
|
14
15
|
export interface ReasonDef { code: string; label: string; cls: ReasonClass; drivers: string[] }
|
|
@@ -61,7 +62,7 @@ interface ParsedScenario { name: string; tags: string[]; manual: boolean; reason
|
|
|
61
62
|
/** Parse scenarios with their tags + the reason comment line above (for @manual). */
|
|
62
63
|
export function parseScenarios(featurePath: string): ParsedScenario[] {
|
|
63
64
|
if (!fs.existsSync(featurePath)) return [];
|
|
64
|
-
const lines =
|
|
65
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
65
66
|
const out: ParsedScenario[] = [];
|
|
66
67
|
for (let i = 0; i < lines.length; i++) {
|
|
67
68
|
const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);
|
|
@@ -113,6 +114,11 @@ export function inferFromText(reason: string): string | undefined {
|
|
|
113
114
|
return undefined;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Every code the text supports, not just the first rule in declaration order. */
|
|
118
|
+
export function inferAllFromText(reason: string): string[] {
|
|
119
|
+
return INFER.filter((r) => r.re.test(reason)).map((r) => r.code);
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
export interface ReasonMismatch { scenario: string; explicit: string; inferred: string }
|
|
117
123
|
|
|
118
124
|
/**
|
|
@@ -123,7 +129,7 @@ export interface ReasonMismatch { scenario: string; explicit: string; inferred:
|
|
|
123
129
|
*/
|
|
124
130
|
export function manualReasonMismatches(featurePath: string): ReasonMismatch[] {
|
|
125
131
|
if (!fs.existsSync(featurePath)) return [];
|
|
126
|
-
const lines =
|
|
132
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
127
133
|
const out: ReasonMismatch[] = [];
|
|
128
134
|
for (let i = 0; i < lines.length; i++) {
|
|
129
135
|
const m = lines[i].match(/^\s*Scenario:\s*(.+)$/);
|
|
@@ -149,8 +155,15 @@ export function manualReasonMismatches(featurePath: string): ReasonMismatch[] {
|
|
|
149
155
|
else if (l === '') continue;
|
|
150
156
|
else break;
|
|
151
157
|
}
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
// The block scanned here includes the tester PROCEDURE, so several codes can
|
|
159
|
+
// match at once — "not worth automating" (M8, the author's actual reason) and
|
|
160
|
+
// "the service stays responsive" (M6, a word about latency, not layout). The
|
|
161
|
+
// first rule in declaration order used to win and the correct tag was reported
|
|
162
|
+
// as a mismatch. A tag is only wrong when the text supports some OTHER code and
|
|
163
|
+
// NOT the declared one.
|
|
164
|
+
const codes = inferAllFromText(parts.join(' '));
|
|
165
|
+
const inferred = codes.find((c) => c !== explicit);
|
|
166
|
+
if (inferred && !codes.includes(explicit)) out.push({ scenario: m[1].trim(), explicit, inferred });
|
|
154
167
|
}
|
|
155
168
|
return out;
|
|
156
169
|
}
|
package/src/harness/feedback.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
export type FeedbackType = 'test-design' | 'product' | 'other';
|
|
17
18
|
export type FeedbackDecision = 'accept' | 'reject' | 'edit' | 'add' | 'none';
|
|
@@ -36,7 +37,7 @@ function currentAuditScore(screen?: string): number | undefined {
|
|
|
36
37
|
if (!screen) return undefined;
|
|
37
38
|
const p = path.join(process.cwd(), '.sungen', 'reports', `${screen}-audit.json`);
|
|
38
39
|
if (!fs.existsSync(p)) return undefined;
|
|
39
|
-
try { return JSON.parse(
|
|
40
|
+
try { return JSON.parse(readTextFile(p))?.score?.overall; } catch { return undefined; }
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function recordFeedback(entry: Omit<FeedbackEntry, 'ts' | 'source' | 'auditScore'> & { ts?: string; source?: string }): string {
|
|
@@ -60,7 +61,7 @@ export function recordFeedback(entry: Omit<FeedbackEntry, 'ts' | 'source' | 'aud
|
|
|
60
61
|
export function readFeedback(): FeedbackEntry[] {
|
|
61
62
|
const p = feedbackPath();
|
|
62
63
|
if (!fs.existsSync(p)) return [];
|
|
63
|
-
return
|
|
64
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export interface FeedbackSummary {
|
|
@@ -13,6 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { loadScenarios } from './parse';
|
|
15
15
|
import { parseScenarios } from './capability-plan';
|
|
16
|
+
import { readTextFile } from './read-text';
|
|
16
17
|
|
|
17
18
|
const KNOWN_TARGETS = ['cart', 'category', 'brand', 'detail', 'checkout', 'wishlist', 'order', 'payment', 'login'];
|
|
18
19
|
|
|
@@ -33,7 +34,7 @@ function featurePath(base: string, kind: 'screens' | 'flows', name: string): str
|
|
|
33
34
|
}
|
|
34
35
|
function featureTags(fp: string): string[] {
|
|
35
36
|
if (!fs.existsSync(fp)) return [];
|
|
36
|
-
for (const line of
|
|
37
|
+
for (const line of readTextFile(fp).split('\n')) {
|
|
37
38
|
const t = line.trim();
|
|
38
39
|
if (t.startsWith('Feature:')) break;
|
|
39
40
|
if (t.startsWith('@')) return t.split(/\s+/).filter((x) => x.startsWith('@'));
|
package/src/harness/flow-plan.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import { parse as parseYaml } from 'yaml';
|
|
13
13
|
import { MANUAL_REASONS, inferReasonCode } from './capability-plan';
|
|
14
14
|
import { buildFlowCheck, Contract } from './flow-check';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
const REF_RE = /\[([A-Za-z][\w ]*?):([\w &.-]+?)\]/g;
|
|
17
18
|
|
|
@@ -19,7 +20,7 @@ interface FlowScenario { name: string; manual: boolean; reason: string; tags: st
|
|
|
19
20
|
|
|
20
21
|
function parseFlowScenarios(featurePath: string): FlowScenario[] {
|
|
21
22
|
if (!fs.existsSync(featurePath)) return [];
|
|
22
|
-
const lines =
|
|
23
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
23
24
|
const out: FlowScenario[] = [];
|
|
24
25
|
const idx: number[] = [];
|
|
25
26
|
lines.forEach((l, i) => { if (/^\s*Scenario:/.test(l)) idx.push(i); });
|
|
@@ -50,7 +51,7 @@ function parseFlowScenarios(featurePath: string): FlowScenario[] {
|
|
|
50
51
|
function selectorKeys(selectorsPath: string): Set<string> {
|
|
51
52
|
if (!fs.existsSync(selectorsPath)) return new Set();
|
|
52
53
|
try {
|
|
53
|
-
const y = parseYaml(
|
|
54
|
+
const y = parseYaml(readTextFile(selectorsPath)) || {};
|
|
54
55
|
return new Set(Object.keys(y).map((k) => k.toLowerCase()));
|
|
55
56
|
} catch { return new Set(); }
|
|
56
57
|
}
|
package/src/harness/intent.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from 'fs';
|
|
16
16
|
import * as path from 'path';
|
|
17
|
+
import { readTextFile } from './read-text';
|
|
17
18
|
|
|
18
19
|
export type IntentFocus = 'functional' | 'e-commerce' | 'security' | 'smoke';
|
|
19
20
|
|
|
@@ -50,7 +51,7 @@ export function readIntent(projectRoot: string): IntentProfile {
|
|
|
50
51
|
const ctx = path.join(projectRoot, 'qa', 'context.md');
|
|
51
52
|
if (!fs.existsSync(ctx)) return DEFAULT_INTENT;
|
|
52
53
|
let text: string;
|
|
53
|
-
try { text =
|
|
54
|
+
try { text = readTextFile(ctx).toLowerCase(); } catch { return DEFAULT_INTENT; }
|
|
54
55
|
|
|
55
56
|
const grab = (key: string): string | undefined => {
|
|
56
57
|
const m = text.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*:\\s*([a-z0-9-]+)`));
|
package/src/harness/journey.ts
CHANGED
|
@@ -16,6 +16,7 @@ import * as fs from 'fs';
|
|
|
16
16
|
import * as path from 'path';
|
|
17
17
|
import * as crypto from 'crypto';
|
|
18
18
|
import { reportSlug } from './unit-paths';
|
|
19
|
+
import { readTextFile } from './read-text';
|
|
19
20
|
|
|
20
21
|
export type ObStatus = 'satisfied' | 'needs-work' | 'pending' | 'waived';
|
|
21
22
|
|
|
@@ -44,13 +45,13 @@ export interface JourneyReport {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
function readJSON(p: string): any {
|
|
47
|
-
try { return fs.existsSync(p) ? JSON.parse(
|
|
48
|
+
try { return fs.existsSync(p) ? JSON.parse(readTextFile(p)) : null; } catch { return null; }
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
function readLedgerPhases(p: string): string[] {
|
|
51
52
|
if (!fs.existsSync(p)) return [];
|
|
52
53
|
const out: string[] = [];
|
|
53
|
-
for (const line of
|
|
54
|
+
for (const line of readTextFile(p).split('\n')) {
|
|
54
55
|
if (!line.trim()) continue;
|
|
55
56
|
try { const d = JSON.parse(line); if (d.step) out.push(String(d.step)); } catch { /* skip */ }
|
|
56
57
|
}
|
package/src/harness/ledger.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { reportSlug } from './unit-paths';
|
|
17
|
+
import { readTextFile } from './read-text';
|
|
17
18
|
|
|
18
19
|
export interface LedgerEvent {
|
|
19
20
|
ts: string;
|
|
@@ -75,7 +76,7 @@ export function recordEvent(screen: string, ev: Omit<LedgerEvent, 'ts'> & { ts?:
|
|
|
75
76
|
export function readEvents(screen: string): LedgerEvent[] {
|
|
76
77
|
const p = ledgerPath(screen);
|
|
77
78
|
if (!fs.existsSync(p)) return [];
|
|
78
|
-
return
|
|
79
|
+
return readTextFile(p).split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export interface LedgerReport {
|
|
@@ -121,7 +122,7 @@ export function buildReport(screen: string, opts: { allRuns?: boolean } = {}): L
|
|
|
121
122
|
const auditPath = path.join(process.cwd(), '.sungen', 'reports', `${reportSlug(screen)}-audit.json`);
|
|
122
123
|
if (fs.existsSync(auditPath)) {
|
|
123
124
|
try {
|
|
124
|
-
const a = JSON.parse(
|
|
125
|
+
const a = JSON.parse(readTextFile(auditPath));
|
|
125
126
|
coveredCritical = a?.gate?.themesCovered ?? null;
|
|
126
127
|
scenarioCount = a?.scenarioCount ?? null;
|
|
127
128
|
} catch { /* ignore */ }
|
package/src/harness/manifest.ts
CHANGED
|
@@ -12,6 +12,7 @@ import * as fs from 'fs';
|
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { createHash } from 'crypto';
|
|
14
14
|
import { featureBasename, reportSlug } from './unit-paths';
|
|
15
|
+
import { readTextFile } from './read-text';
|
|
15
16
|
|
|
16
17
|
export interface SpecSection { name: string; hash: string }
|
|
17
18
|
export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
|
|
@@ -59,7 +60,7 @@ function matchSection(featureSection: string, specNames: string[]): string | nul
|
|
|
59
60
|
/** Parse spec.md into hashable sections (## and ### headings; "Section:" prefix stripped). */
|
|
60
61
|
export function parseSpecSections(specPath: string): SpecSection[] {
|
|
61
62
|
if (!fs.existsSync(specPath)) return [];
|
|
62
|
-
const lines =
|
|
63
|
+
const lines = readTextFile(specPath).split('\n');
|
|
63
64
|
const sections: { name: string; body: string[] }[] = [];
|
|
64
65
|
let cur: { name: string; body: string[] } | null = null;
|
|
65
66
|
for (const line of lines) {
|
|
@@ -79,7 +80,7 @@ export function parseSpecSections(specPath: string): SpecSection[] {
|
|
|
79
80
|
/** Parse feature into scenario → section mapping using `# --- Section: X ---` comments. */
|
|
80
81
|
function parseFeatureSections(featurePath: string): { scenario: string; vpCode?: string; section: string }[] {
|
|
81
82
|
if (!fs.existsSync(featurePath)) return [];
|
|
82
|
-
const lines =
|
|
83
|
+
const lines = readTextFile(featurePath).split('\n');
|
|
83
84
|
const out: { scenario: string; vpCode?: string; section: string }[] = [];
|
|
84
85
|
let section = '(unsectioned)';
|
|
85
86
|
for (const raw of lines) {
|
|
@@ -164,7 +165,7 @@ export function manifestPath(screenName: string): string {
|
|
|
164
165
|
}
|
|
165
166
|
export function loadManifest(screenName: string): Manifest | null {
|
|
166
167
|
const p = manifestPath(screenName);
|
|
167
|
-
return fs.existsSync(p) ? JSON.parse(
|
|
168
|
+
return fs.existsSync(p) ? JSON.parse(readTextFile(p)) : null;
|
|
168
169
|
}
|
|
169
170
|
export function saveManifest(m: Manifest): string {
|
|
170
171
|
const p = manifestPath(m.screen);
|
package/src/harness/parse.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { GherkinParser, ParsedScenario, ParsedStep } from '../generators/gherkin-parser';
|
|
10
|
+
import { readTextFile } from './read-text';
|
|
10
11
|
|
|
11
12
|
export type Priority = 'high' | 'normal' | 'low' | 'unknown';
|
|
12
13
|
|
|
@@ -51,9 +52,22 @@ export function idPrefix(id: string): string {
|
|
|
51
52
|
|
|
52
53
|
// ---------- test-viewpoint.md ----------
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* A viewpoint OVERVIEW id may be a bare category (`VP-LOGIC`, `VP-SEC`) — that is
|
|
57
|
+
* the scheme sungen itself prescribes, with the sequence number living on the
|
|
58
|
+
* scenario (`VP-LOGIC-013`). `isIdLike` demands a digit to avoid picking prose out
|
|
59
|
+
* of tables, which silently rejected every category id: a project declaring
|
|
60
|
+
* VP-LOGIC/VP-VAL/VP-SEC/VP-UI/VP-NAV/VP-PERF/VP-I18N parsed as ONE viewpoint
|
|
61
|
+
* (only VP-I18N, for the "18"), traceability collapsed to ~0%, and the audit told
|
|
62
|
+
* QA to re-tag scenarios that were already correct.
|
|
63
|
+
*/
|
|
64
|
+
function isViewpointId(s: string): boolean {
|
|
65
|
+
return isIdLike(s) || /^VP-[A-Z][A-Z0-9]*$/i.test(s);
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
55
69
|
if (!fs.existsSync(filePath)) return [];
|
|
56
|
-
const text =
|
|
70
|
+
const text = readTextFile(filePath);
|
|
57
71
|
const lines = text.split('\n');
|
|
58
72
|
|
|
59
73
|
const entries = new Map<string, ViewpointEntry>();
|
|
@@ -68,7 +82,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
68
82
|
const cells = line.split('|').map((c) => c.trim()).filter((_, i, a) => i > 0 && i < a.length - 1);
|
|
69
83
|
if (cells.length >= 3) {
|
|
70
84
|
const id = cells[0];
|
|
71
|
-
if (
|
|
85
|
+
if (isViewpointId(id) && !/^-+$/.test(cells[1])) {
|
|
72
86
|
const pr = /high/i.test(cells[1]) ? 'High' : /medium/i.test(cells[1]) ? 'Medium' : /low/i.test(cells[1]) ? 'Low' : 'Unknown';
|
|
73
87
|
entries.set(id.toUpperCase(), { id: id.toUpperCase(), priority: pr as any, reason: cells[2] });
|
|
74
88
|
}
|
|
@@ -85,7 +99,7 @@ export function parseViewpointOverview(filePath: string): ViewpointEntry[] {
|
|
|
85
99
|
if (/^##\s/.test(line)) { group = undefined; }
|
|
86
100
|
if (group) {
|
|
87
101
|
const m = line.match(/^[-*+]\s+([A-Za-z][A-Za-z0-9.-]*)/);
|
|
88
|
-
if (m &&
|
|
102
|
+
if (m && isViewpointId(m[1])) {
|
|
89
103
|
const id = m[1].toUpperCase();
|
|
90
104
|
const existing = entries.get(id);
|
|
91
105
|
if (existing) existing.group = group;
|
|
@@ -7,6 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as path from 'path';
|
|
8
8
|
import { ScenarioInfo, loadScenarios, idPrefix } from './parse';
|
|
9
9
|
import { parseManualComments } from '../exporters/scenario-merger';
|
|
10
|
+
import { readTextFile } from './read-text';
|
|
10
11
|
|
|
11
12
|
// ---------- #2 Downstream-scope ----------
|
|
12
13
|
|
|
@@ -202,5 +203,5 @@ export function crossArtifactOwnership(screenDir: string, scenarios: ScenarioInf
|
|
|
202
203
|
|
|
203
204
|
// convenience reader
|
|
204
205
|
export function readText(p: string): string {
|
|
205
|
-
return fs.existsSync(p) ?
|
|
206
|
+
return fs.existsSync(p) ? readTextFile(p) : '';
|
|
206
207
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Read a user-authored text file with line endings NORMALISED to `\n`.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: the harness parses spec.md / test-viewpoint.md / .feature with
|
|
7
|
+
* line-anchored regexes such as `/^\s*Scenario:\s*(.+)$/`. In JavaScript `.` does
|
|
8
|
+
* NOT match `\r`, so on a CRLF file `(.+)$` can never reach the end of the line
|
|
9
|
+
* and the match silently fails. The parser then reports "nothing found" rather
|
|
10
|
+
* than an error, which is far worse than crashing:
|
|
11
|
+
*
|
|
12
|
+
* - `spec-coverage` found 0 requirements → the FR sensor scored a vacuous
|
|
13
|
+
* 100% and never emitted SPEC-UNCOVERED / SPEC-TRACE-IMPLICIT.
|
|
14
|
+
* - `parseViewpointOverview` found 1 of N viewpoints → traceability collapsed
|
|
15
|
+
* to ~0% and the audit told QA to "re-tag" scenarios that were already
|
|
16
|
+
* correct.
|
|
17
|
+
*
|
|
18
|
+
* A Windows-authored spec is completely normal, so every harness parser that
|
|
19
|
+
* reads a user file must go through here.
|
|
20
|
+
*/
|
|
21
|
+
export function readTextFile(filePath: string): string {
|
|
22
|
+
return fs.readFileSync(filePath, 'utf-8').replace(/\r\n?/g, '\n');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `readTextFile` split into lines — the common case for line-anchored parsers. */
|
|
26
|
+
export function readTextLines(filePath: string): string[] {
|
|
27
|
+
return readTextFile(filePath).split('\n');
|
|
28
|
+
}
|
|
@@ -17,6 +17,7 @@ import * as path from 'path';
|
|
|
17
17
|
import * as os from 'os';
|
|
18
18
|
import { loadScenarios, ScenarioInfo } from './parse';
|
|
19
19
|
import { featureBasename } from './unit-paths';
|
|
20
|
+
import { readTextFile } from './read-text';
|
|
20
21
|
|
|
21
22
|
export interface ScriptCheckResult {
|
|
22
23
|
screen: string;
|
|
@@ -153,7 +154,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, kind
|
|
|
153
154
|
let specTitles: string[] = [];
|
|
154
155
|
let specSrc = '';
|
|
155
156
|
if (committedSpec) {
|
|
156
|
-
specSrc =
|
|
157
|
+
specSrc = readTextFile(committedSpec);
|
|
157
158
|
specTitles = extractTestTitles(specSrc);
|
|
158
159
|
} else {
|
|
159
160
|
findings.push('No generated spec found under specs/generated/ — run `sungen generate` / `/sungen:run-test` first.');
|
|
@@ -195,7 +196,7 @@ export async function runScriptCheck(screenDir: string, screenName: string, kind
|
|
|
195
196
|
const fresh = findSpec(tmp, screenName, kind);
|
|
196
197
|
if (fresh) {
|
|
197
198
|
const a = normalize(specSrc);
|
|
198
|
-
const b = normalize(
|
|
199
|
+
const b = normalize(readTextFile(fresh));
|
|
199
200
|
if (a !== b) {
|
|
200
201
|
drift = 'drift';
|
|
201
202
|
// collect a few differing lines
|