@sun-asterisk/sungen 3.2.24-beta.2 → 3.2.24-beta.4
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 +5 -14
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +11 -12
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/cli/commands/challenge.d.ts.map +1 -1
- package/dist/cli/commands/challenge.js +2 -10
- package/dist/cli/commands/challenge.js.map +1 -1
- package/dist/cli/commands/depth-lint.d.ts.map +1 -1
- package/dist/cli/commands/depth-lint.js +2 -12
- package/dist/cli/commands/depth-lint.js.map +1 -1
- package/dist/cli/commands/gate.d.ts.map +1 -1
- package/dist/cli/commands/gate.js +2 -47
- package/dist/cli/commands/gate.js.map +1 -1
- package/dist/cli/commands/journey.d.ts.map +1 -1
- package/dist/cli/commands/journey.js +2 -12
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/manifest.d.ts.map +1 -1
- package/dist/cli/commands/manifest.js +2 -10
- package/dist/cli/commands/manifest.js.map +1 -1
- package/dist/cli/commands/next.d.ts +14 -0
- package/dist/cli/commands/next.d.ts.map +1 -0
- package/dist/cli/commands/next.js +130 -0
- package/dist/cli/commands/next.js.map +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/resolve-unit.d.ts +22 -0
- package/dist/cli/resolve-unit.d.ts.map +1 -0
- package/dist/cli/resolve-unit.js +101 -0
- package/dist/cli/resolve-unit.js.map +1 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +39 -2
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +12 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +16 -2
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/capability.d.ts +12 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js +16 -0
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +5 -0
- package/dist/harness/flow-contract.d.ts +16 -1
- package/dist/harness/flow-contract.d.ts.map +1 -1
- package/dist/harness/flow-contract.js +8 -1
- package/dist/harness/flow-contract.js.map +1 -1
- package/dist/harness/next-step.d.ts +40 -0
- package/dist/harness/next-step.d.ts.map +1 -0
- package/dist/harness/next-step.js +242 -0
- package/dist/harness/next-step.js.map +1 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +13 -0
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +35 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +40 -0
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +14 -6
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +26 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +5 -12
- package/src/cli/commands/capability.ts +11 -10
- package/src/cli/commands/challenge.ts +2 -8
- package/src/cli/commands/depth-lint.ts +2 -10
- package/src/cli/commands/gate.ts +2 -10
- package/src/cli/commands/journey.ts +2 -10
- package/src/cli/commands/manifest.ts +2 -8
- package/src/cli/commands/next.ts +96 -0
- package/src/cli/index.ts +31 -0
- package/src/cli/resolve-unit.ts +72 -0
- package/src/harness/audit.ts +39 -3
- package/src/harness/capability-plan.ts +23 -3
- package/src/harness/capability.ts +22 -0
- package/src/harness/catalog/drivers.yaml +5 -0
- package/src/harness/flow-contract.ts +24 -3
- package/src/harness/next-step.ts +237 -0
- package/src/harness/sensors.ts +13 -0
- package/src/harness/viewpoint-ledger.ts +65 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +14 -6
- package/src/orchestrator/templates/ai-src/commands/run-test.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +26 -0
|
@@ -235,8 +235,22 @@ export function flowIdOf(s: ScenarioInfo, declaredPhases: string[]): string | nu
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
export interface InventoryResult {
|
|
238
|
-
/**
|
|
239
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Declared → the scenarios claiming it. A declared flow with none is a NAMED gap.
|
|
240
|
+
* `automated` is false when EVERY scenario claiming the flow is `@manual`: the flow is
|
|
241
|
+
* accounted for, but nothing runs — see `manualOnly` (#599).
|
|
242
|
+
*/
|
|
243
|
+
covered: Array<{ id: string; scenarios: string[]; automated: boolean }>;
|
|
244
|
+
/**
|
|
245
|
+
* Declared `covered` and claimed only by `@manual` scenarios. `covered` used to mean three
|
|
246
|
+
* different things at once — a flow with three automated scenarios, one with a single manual
|
|
247
|
+
* procedure, and one whose oracle is still an open question all read as `covered`, and
|
|
248
|
+
* `flowCoverage` reported 100% over the mix. A reader cannot tell which flows would actually
|
|
249
|
+
* fail a regression run.
|
|
250
|
+
*/
|
|
251
|
+
manualOnly: FlowDecl[];
|
|
252
|
+
/** Covered declared flows with at least one AUTOMATED scenario / flows that ought to be. */
|
|
253
|
+
automatedRatio: number;
|
|
240
254
|
/** Declared `covered` but nothing written — the silent-absence case, now named. */
|
|
241
255
|
uncovered: FlowDecl[];
|
|
242
256
|
/** Declared with a non-covered status, carried into the report so it stays visible. */
|
|
@@ -271,9 +285,16 @@ export function flowInventory(contract: FlowContract, scenarios: ScenarioInfo[])
|
|
|
271
285
|
}
|
|
272
286
|
const expected = contract.flows.filter((f) => f.status === 'covered');
|
|
273
287
|
const covered = expected.filter((f) => (byId.get(f.id) ?? []).length > 0)
|
|
274
|
-
.map((f) => ({
|
|
288
|
+
.map((f) => ({
|
|
289
|
+
id: f.id,
|
|
290
|
+
scenarios: byId.get(f.id)!,
|
|
291
|
+
automated: scenarios.some((sc) => flowIdOf(sc, contract.phases) === f.id && !sc.manual),
|
|
292
|
+
}));
|
|
293
|
+
const manualOnly = covered.filter((c) => !c.automated).map((c) => expected.find((f) => f.id === c.id)!);
|
|
275
294
|
return {
|
|
276
295
|
covered,
|
|
296
|
+
manualOnly,
|
|
297
|
+
automatedRatio: expected.length ? covered.filter((c) => c.automated).length / expected.length : 1,
|
|
277
298
|
uncovered: expected.filter((f) => (byId.get(f.id) ?? []).length === 0),
|
|
278
299
|
accounted: contract.flows.filter((f) => f.status !== 'covered'),
|
|
279
300
|
undeclared,
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "What now?" — one deterministic resolver over the project's own state.
|
|
3
|
+
*
|
|
4
|
+
* The next step used to be hardcoded per command, and only two of twenty-nine printed one at
|
|
5
|
+
* all. So a run that ended anywhere else ended in silence: `sungen capability add mock`
|
|
6
|
+
* finished with a blank line, the operator's agent had nothing telling it what remained, and the
|
|
7
|
+
* whole session stopped mid-workflow (#597).
|
|
8
|
+
*
|
|
9
|
+
* Hardcoding it in the other twenty-seven would repeat the mistake in bulk. What decides the
|
|
10
|
+
* next step is not which command just ran — it is what the project now HOLDS: the artifacts on
|
|
11
|
+
* disk, the audit report in `.sungen/reports/`, the accepted viewpoint baseline, the compiled
|
|
12
|
+
* spec, the run results. So this reads that state and derives the answer, and every command ends
|
|
13
|
+
* by pointing here. One place to be right, one place to test.
|
|
14
|
+
*
|
|
15
|
+
* Deterministic and read-only: same project state → same answer, no network, no writes.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { reportSlug, featureFilesFor } from './unit-paths';
|
|
20
|
+
import { readTextFile } from './read-text';
|
|
21
|
+
|
|
22
|
+
export interface NextStep {
|
|
23
|
+
/** The command to run, as the operator would type it. */
|
|
24
|
+
command: string;
|
|
25
|
+
/** Why it is next — stated so an agent can weigh it, not just obey it. */
|
|
26
|
+
because: string;
|
|
27
|
+
/** `blocked` steps are not runnable yet; they name what to resolve first. */
|
|
28
|
+
kind: 'do' | 'blocked' | 'optional';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UnitState {
|
|
32
|
+
unit: string;
|
|
33
|
+
kind: 'screen' | 'flow' | 'api';
|
|
34
|
+
hasSpec: boolean;
|
|
35
|
+
hasViewpoint: boolean;
|
|
36
|
+
hasContract: boolean;
|
|
37
|
+
hasFlowInventory: boolean;
|
|
38
|
+
scenarioCount: number;
|
|
39
|
+
hasSelectors: boolean;
|
|
40
|
+
selectorsArePlaceholder: boolean;
|
|
41
|
+
compiled: boolean;
|
|
42
|
+
hasResults: boolean;
|
|
43
|
+
hasDeliverable: boolean;
|
|
44
|
+
audit?: {
|
|
45
|
+
overall: number;
|
|
46
|
+
gateStatus: string;
|
|
47
|
+
findings: string[];
|
|
48
|
+
viewpointBaselineStatus?: string;
|
|
49
|
+
};
|
|
50
|
+
steps: NextStep[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const exists = (p: string): boolean => fs.existsSync(p);
|
|
54
|
+
|
|
55
|
+
function readJson(p: string): Record<string, unknown> | null {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readTextFile(p)) as Record<string, unknown>;
|
|
58
|
+
} catch {
|
|
59
|
+
return null; // absent or unreadable — treated as "not run"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function countScenarios(unitDir: string, unit: string): number {
|
|
64
|
+
let n = 0;
|
|
65
|
+
for (const f of featureFilesFor(unitDir, unit)) {
|
|
66
|
+
const text = readTextFile(f) ?? '';
|
|
67
|
+
n += (text.match(/^\s*Scenario(?: Outline)?:/gm) ?? []).length;
|
|
68
|
+
}
|
|
69
|
+
return n;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Everything the resolver needs, read from disk. `unitId` is the catalog id
|
|
74
|
+
* (`<screen>` · `flows/<f>` · `api/<a>`); `unitDir` is its directory.
|
|
75
|
+
*/
|
|
76
|
+
export function readUnitState(
|
|
77
|
+
projectRoot: string, unitDir: string, unit: string, kind: UnitState['kind'],
|
|
78
|
+
): UnitState {
|
|
79
|
+
const req = path.join(unitDir, 'requirements');
|
|
80
|
+
const slug = reportSlug(unit);
|
|
81
|
+
const audit = readJson(path.join(projectRoot, '.sungen', 'reports', `${slug}-audit.json`));
|
|
82
|
+
const selectorsDir = path.join(unitDir, 'selectors');
|
|
83
|
+
let selectorFiles: string[] = [];
|
|
84
|
+
try {
|
|
85
|
+
selectorFiles = fs.readdirSync(selectorsDir).filter((f) => f.endsWith('.yaml'));
|
|
86
|
+
} catch { /* api units carry no selectors */ }
|
|
87
|
+
const selectorText = selectorFiles.map((f) => readTextFile(path.join(selectorsDir, f)) ?? '').join('\n');
|
|
88
|
+
const contractPath = path.join(req, 'flow-contract.yaml');
|
|
89
|
+
const contractText = readTextFile(contractPath) ?? '';
|
|
90
|
+
|
|
91
|
+
const specDirs = [
|
|
92
|
+
path.join(projectRoot, 'specs', 'generated', 'flows', unit),
|
|
93
|
+
path.join(projectRoot, 'specs', 'generated', unit),
|
|
94
|
+
];
|
|
95
|
+
const compiled = specDirs.some((d) => {
|
|
96
|
+
try {
|
|
97
|
+
return fs.readdirSync(d).some((f) => f.endsWith('.spec.ts'));
|
|
98
|
+
} catch { return false; }
|
|
99
|
+
});
|
|
100
|
+
const hasResults = specDirs.some((d) => {
|
|
101
|
+
try {
|
|
102
|
+
return fs.readdirSync(d).some((f) => f.endsWith('-test-result.json'));
|
|
103
|
+
} catch { return false; }
|
|
104
|
+
});
|
|
105
|
+
let hasDeliverable = false;
|
|
106
|
+
try {
|
|
107
|
+
hasDeliverable = fs.readdirSync(path.join(projectRoot, 'qa', 'deliverables'))
|
|
108
|
+
.some((f) => f.startsWith(`${slug}-`));
|
|
109
|
+
} catch { /* nothing exported yet */ }
|
|
110
|
+
|
|
111
|
+
const score = (audit?.score ?? {}) as { overall?: number };
|
|
112
|
+
const baseline = (audit?.viewpointBaseline ?? {}) as { status?: string };
|
|
113
|
+
const state: UnitState = {
|
|
114
|
+
unit, kind,
|
|
115
|
+
hasSpec: exists(path.join(req, 'spec.md')),
|
|
116
|
+
hasViewpoint: exists(path.join(req, 'test-viewpoint.md')),
|
|
117
|
+
hasContract: exists(contractPath),
|
|
118
|
+
// A contract WITHOUT `flows:` can only be scored per phase, which is a materially weaker
|
|
119
|
+
// claim — so it is part of the state, not a detail.
|
|
120
|
+
hasFlowInventory: /^\s*flows:\s*$/m.test(contractText),
|
|
121
|
+
scenarioCount: countScenarios(unitDir, unit),
|
|
122
|
+
hasSelectors: selectorFiles.length > 0,
|
|
123
|
+
// A scaffolded selector file exists but resolves nothing; treating it as "done" is what
|
|
124
|
+
// makes a run look further along than it is.
|
|
125
|
+
selectorsArePlaceholder: selectorFiles.length > 0
|
|
126
|
+
&& (/TODO|PLACEHOLDER|REPLACE ME/i.test(selectorText) || selectorText.trim().length < 40),
|
|
127
|
+
compiled, hasResults, hasDeliverable,
|
|
128
|
+
audit: audit
|
|
129
|
+
? {
|
|
130
|
+
overall: typeof score.overall === 'number' ? score.overall : 0,
|
|
131
|
+
gateStatus: String(audit.gateStatus ?? 'UNKNOWN'),
|
|
132
|
+
findings: Array.isArray(audit.findings) ? (audit.findings as string[]) : [],
|
|
133
|
+
viewpointBaselineStatus: baseline.status,
|
|
134
|
+
}
|
|
135
|
+
: undefined,
|
|
136
|
+
steps: [],
|
|
137
|
+
};
|
|
138
|
+
state.steps = deriveSteps(state);
|
|
139
|
+
return state;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The finding codes worth naming in a next step — each maps to a specific repair. */
|
|
143
|
+
const REPAIRABLE = [
|
|
144
|
+
'FLOW-CONTRACT-MISSING', 'FLOW-INVENTORY-MISSING', 'FLOW-UNCOVERED', 'FLOW-UNDECLARED',
|
|
145
|
+
'FLOW-PHASE-MISFILED', 'FLOW-GUARANTEE-MISSING', 'FLOW-OUTCOME-UNPROVEN', 'CONTINUITY-ONE-SIDED',
|
|
146
|
+
'SPEC-RESTATED-UNVERIFIED', 'VIEWPOINT-GESTURE-SUBSTITUTED', 'SPEC-UNCOVERED', 'TRIGGER-UNCOVERED',
|
|
147
|
+
'VIEWPOINT-ITEM-MISSING', 'MANUAL-CODE-MISSING', 'MANUAL-AUTOMATABLE', 'DEPTH-DEFERRED',
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The ordered answer. Earliest unmet precondition first, so the list reads as a path rather than
|
|
152
|
+
* a menu — and a `blocked` step names what to resolve instead of pretending it can run.
|
|
153
|
+
*/
|
|
154
|
+
export function deriveSteps(s: UnitState): NextStep[] {
|
|
155
|
+
const out: NextStep[] = [];
|
|
156
|
+
const design = s.kind === 'api' ? `/sungen:create-test ${s.unit}` : `/sungen:create-test ${s.unit}`;
|
|
157
|
+
|
|
158
|
+
if (!s.hasSpec && !s.hasViewpoint) {
|
|
159
|
+
out.push({
|
|
160
|
+
kind: 'blocked',
|
|
161
|
+
command: `write qa/${s.kind === 'screen' ? 'screens' : s.kind === 'flow' ? 'flows' : 'api'}/${s.unit}/requirements/spec.md`,
|
|
162
|
+
because: 'the unit has neither a spec nor a test-viewpoint, so there is nothing to generate FROM — generation without a source invents behaviour',
|
|
163
|
+
});
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
if (s.kind === 'flow' && !s.hasContract) {
|
|
167
|
+
out.push({
|
|
168
|
+
kind: 'blocked',
|
|
169
|
+
command: `author requirements/flow-contract.yaml, then ${design}`,
|
|
170
|
+
because: 'a flow with no contract is scored as a generic screen against page-type themes a journey cannot have (FLOW-CONTRACT-MISSING)',
|
|
171
|
+
});
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
if (s.scenarioCount === 0) {
|
|
175
|
+
out.push({ kind: 'do', command: design, because: 'no scenarios exist yet' });
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
if (s.kind === 'flow' && !s.hasFlowInventory) {
|
|
179
|
+
out.push({
|
|
180
|
+
kind: 'do',
|
|
181
|
+
command: `add \`flows:\` to requirements/flow-contract.yaml, then sungen audit --screen ${s.unit}`,
|
|
182
|
+
because: 'without the inventory, coverage is measured per PHASE — and a phase counts as covered as soon as one scenario carries it, so the number cannot tell you a flow is missing',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
if (!s.audit) {
|
|
186
|
+
out.push({ kind: 'do', command: `sungen audit --screen ${s.unit}`, because: 'the suite has never been measured' });
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
if (s.audit.viewpointBaselineStatus === 'changed') {
|
|
190
|
+
out.push({
|
|
191
|
+
kind: 'do',
|
|
192
|
+
command: `sungen audit --screen ${s.unit} --accept-viewpoint`,
|
|
193
|
+
because: 'test-viewpoint.md changed since the accepted baseline, so the ledger and traceability axes are not evidence until you confirm the new declaration',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const repair = s.audit.findings
|
|
197
|
+
.map((f) => f.split(':')[0].trim())
|
|
198
|
+
.filter((code) => REPAIRABLE.includes(code));
|
|
199
|
+
if (repair.length > 0) {
|
|
200
|
+
out.push({
|
|
201
|
+
kind: 'do',
|
|
202
|
+
command: `repair, then sungen audit --screen ${s.unit}`,
|
|
203
|
+
because: `${repair.length} finding(s) name a specific repair: ${[...new Set(repair)].slice(0, 4).join(', ')}`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (s.audit.gateStatus === 'FAIL') {
|
|
207
|
+
out.push({
|
|
208
|
+
kind: 'blocked',
|
|
209
|
+
command: `repair the gate, then sungen audit --screen ${s.unit}`,
|
|
210
|
+
because: `the viewpoint gate is FAIL at ${s.audit.overall}/10 — running tests against a design that fails its own gate measures the wrong thing`,
|
|
211
|
+
});
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
if (s.kind !== 'api' && (!s.hasSelectors || s.selectorsArePlaceholder)) {
|
|
215
|
+
out.push({
|
|
216
|
+
kind: 'do',
|
|
217
|
+
command: `/sungen:run-test ${s.unit}`,
|
|
218
|
+
because: s.hasSelectors
|
|
219
|
+
? 'the selector file is still a placeholder, so nothing resolves against a live page'
|
|
220
|
+
: 'no selectors exist yet — run-test generates them from the live page, compiles and auto-fixes',
|
|
221
|
+
});
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
if (!s.compiled) {
|
|
225
|
+
out.push({ kind: 'do', command: `sungen generate --${s.kind === 'flow' ? 'flow' : 'screen'} ${s.unit}`, because: 'the suite has not been compiled to a spec yet' });
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
if (!s.hasResults) {
|
|
229
|
+
out.push({ kind: 'do', command: `/sungen:run-test ${s.unit}`, because: 'the compiled spec has never been run, so every case is Pending' });
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
if (!s.hasDeliverable) {
|
|
233
|
+
out.push({ kind: 'do', command: `/sungen:delivery ${s.unit}`, because: 'results exist but have not been exported to the Test Case & Coverage Matrix' });
|
|
234
|
+
}
|
|
235
|
+
out.push({ kind: 'optional', command: `/sungen:create-test ${s.unit}`, because: 'extend coverage — the next tier, or the flows the inventory still lists as pending' });
|
|
236
|
+
return out;
|
|
237
|
+
}
|
package/src/harness/sensors.ts
CHANGED
|
@@ -588,6 +588,19 @@ interface ClaimRule {
|
|
|
588
588
|
|
|
589
589
|
// Ordered by specificity; the first matching rule per scenario wins.
|
|
590
590
|
const CLAIM_RULES: ClaimRule[] = [
|
|
591
|
+
{
|
|
592
|
+
// A continuity claim is an EXACTNESS claim. `text contains {{v}}` passes when the UI
|
|
593
|
+
// concatenates, prefixes or truncates — which is exactly the defect "unchanged" exists to
|
|
594
|
+
// catch, so proving it with a substring proves the opposite of what the title says. Found on
|
|
595
|
+
// a real basic flow whose seven confirmation rows all used `contains` while claiming the six
|
|
596
|
+
// values "survive unchanged" (#599).
|
|
597
|
+
claim: 'unchanged/preserved',
|
|
598
|
+
title: /\b(unchanged|unmodified|un-?edited|identical|preserved|intact|verbatim|as entered|without (?:any )?(?:change|modification))\b/,
|
|
599
|
+
proof: /^(?![\s\S]*\bcontains \{\{)/,
|
|
600
|
+
need: 'exact-value assertions (`see [X] text with {{v}}` / `field with {{v}}`) for the values the title says are unchanged',
|
|
601
|
+
hint: 'swap `contains {{v}}` for `with {{v}}` on the continuity rows. A substring match cannot distinguish "unchanged" from "changed by adding a prefix" — and if the screen genuinely renders extra text around the value, say so in the title instead of claiming exactness.',
|
|
602
|
+
severity: 'warn',
|
|
603
|
+
},
|
|
591
604
|
{
|
|
592
605
|
claim: 'all/every/each',
|
|
593
606
|
title: /\b(all|every|each)\b/,
|
|
@@ -136,3 +136,68 @@ export function browserGestureSubstitutions(
|
|
|
136
136
|
}
|
|
137
137
|
return out;
|
|
138
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Traceability integrity of the DECLARATION itself — three ways a viewpoint document can be
|
|
142
|
+
* internally wrong while every coverage number still reads well (#599).
|
|
143
|
+
*
|
|
144
|
+
* These are cheap and deterministic, and each was found on a real generated document:
|
|
145
|
+
*
|
|
146
|
+
* - **A reused item id.** `FL-BF-001` labelled four different atomic claims. The ledger then
|
|
147
|
+
* counts four items while traceability sees one id, so coverage arithmetic and "which test
|
|
148
|
+
* proves this?" both go wrong — and neither number looks suspicious.
|
|
149
|
+
* - **A dangling scenario reference.** A `test-data` comment cited `VP-LOGIC-EF07-001` when the
|
|
150
|
+
* suite spells it `VP-SEC-EF07-001`. Nothing breaks at runtime, so the wrong pointer survives
|
|
151
|
+
* every refactor and quietly misleads the next maintainer.
|
|
152
|
+
* - **A uniform priority table.** Every declared phase marked `High` is not a priority, it is a
|
|
153
|
+
* formality: nothing can be selected for a smoke run, and the release conversation has no
|
|
154
|
+
* input. A real ranking has at least two levels.
|
|
155
|
+
*/
|
|
156
|
+
export interface ViewpointIntegrity {
|
|
157
|
+
duplicateIds: Array<{ id: string; count: number; texts: string[] }>;
|
|
158
|
+
danglingRefs: Array<{ ref: string; where: string }>;
|
|
159
|
+
uniformPriority: { levels: string[]; rows: number } | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** `VP-…`/`FL-…` ids as cited in prose — the shape scenario titles use. */
|
|
163
|
+
const CITED_ID = /\b((?:VP|FL)-[A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*-\d+)\b/g;
|
|
164
|
+
|
|
165
|
+
export function viewpointIntegrity(
|
|
166
|
+
viewpointPath: string, scenarioIds: string[], extraSources: Array<{ label: string; text: string }> = [],
|
|
167
|
+
): ViewpointIntegrity {
|
|
168
|
+
const items = parseViewpointItems(viewpointPath);
|
|
169
|
+
const byId = new Map<string, string[]>();
|
|
170
|
+
for (const it of items) {
|
|
171
|
+
if (!it.id) continue;
|
|
172
|
+
byId.set(it.id, [...(byId.get(it.id) ?? []), it.text]);
|
|
173
|
+
}
|
|
174
|
+
const duplicateIds = [...byId.entries()]
|
|
175
|
+
.filter(([, texts]) => texts.length > 1)
|
|
176
|
+
.map(([id, texts]) => ({ id, count: texts.length, texts }))
|
|
177
|
+
.sort((a, b) => b.count - a.count);
|
|
178
|
+
|
|
179
|
+
// A cited id must name a scenario that exists. Compared case-insensitively; the viewpoint's own
|
|
180
|
+
// declaration ids are NOT scenario references, so only the extra sources (test-data, spec) are
|
|
181
|
+
// scanned — a viewpoint legitimately declares ids the suite then implements.
|
|
182
|
+
const known = new Set(scenarioIds.map((s) => s.toUpperCase()));
|
|
183
|
+
const danglingRefs: Array<{ ref: string; where: string }> = [];
|
|
184
|
+
for (const src of extraSources) {
|
|
185
|
+
for (const m of src.text.matchAll(CITED_ID)) {
|
|
186
|
+
const ref = m[1].toUpperCase();
|
|
187
|
+
if (known.has(ref)) continue;
|
|
188
|
+
if (danglingRefs.some((d) => d.ref === ref && d.where === src.label)) continue;
|
|
189
|
+
danglingRefs.push({ ref: m[1], where: src.label });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The Priority-Viewpoints table: `| <id> | <priority> | <reason> |`.
|
|
194
|
+
// `readTextFile` throws on a missing file, and a unit may legitimately have no viewpoint yet.
|
|
195
|
+
const vpText = fs.existsSync(viewpointPath) ? readTextFile(viewpointPath) : '';
|
|
196
|
+
const rows = [...vpText
|
|
197
|
+
.matchAll(/^\|\s*((?:VP|FL)[A-Z0-9._-]*)\s*\|\s*(critical|high|medium|normal|low|deferred)\s*\|/gim)]
|
|
198
|
+
.map((m) => m[2].toLowerCase());
|
|
199
|
+
const levels = [...new Set(rows)];
|
|
200
|
+
const uniformPriority = rows.length >= 3 && levels.length === 1 ? { levels, rows: rows.length } : null;
|
|
201
|
+
|
|
202
|
+
return { duplicateIds, danglingRefs, uniformPriority };
|
|
203
|
+
}
|
|
@@ -9,12 +9,20 @@ codex-trigger: "Run when the user asks to CREATE, generate, write, or author tes
|
|
|
9
9
|
---
|
|
10
10
|
## ⛔ HARD RULE — the run's LAST action is the next-step hand-back
|
|
11
11
|
|
|
12
|
-
A
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
A run is NOT finished when its files are written or its output prints. The final action of EVERY
|
|
13
|
+
run — success, partial, or aborted — is the next-step hand-back.
|
|
14
|
+
|
|
15
|
+
**Ask `sungen next <unit>` rather than deciding from memory.** It derives the answer from what the
|
|
16
|
+
project actually holds (artifacts on disk, the audit report, the accepted viewpoint baseline, the
|
|
17
|
+
compiled spec, the run results), so the hand-back stays correct even when the run ended somewhere
|
|
18
|
+
unexpected — a failed install, a driver that turned out not to support this unit kind, an aborted
|
|
19
|
+
repair loop. Every `sungen` command also prints `Next: sungen next …` as its own last line.
|
|
20
|
+
|
|
21
|
+
This matters most exactly where it used to break. A run that ended on a command with no footer of
|
|
22
|
+
its own — `sungen capability add`, a dead-end install — left the session with nothing to follow and
|
|
23
|
+
it stopped mid-workflow. If a step turns out to be impossible, that is still a hand-back: say what
|
|
24
|
+
blocked it, run `sungen next <unit>`, and offer what remains. Ending with a prose summary and no
|
|
25
|
+
choices is a broken run.
|
|
18
26
|
|
|
19
27
|
---
|
|
20
28
|
{{#cap parallel-subagents}}
|
|
@@ -27,6 +27,12 @@ inside the run, not reasons to stop and hand the work back.
|
|
|
27
27
|
install command as the hand-back.
|
|
28
28
|
- Ending this run follows the same law as create-test: the LAST action is the next-step
|
|
29
29
|
hand-back (the AskUserQuestion in "After showing results"), no matter how the run went.
|
|
30
|
+
**Ask `sungen next <unit>` rather than deciding from memory** — it derives the answer from what
|
|
31
|
+
the project now holds, so the hand-back stays correct even when the run ended somewhere
|
|
32
|
+
unexpected (a failed install, a driver that does not support this unit kind, an aborted repair
|
|
33
|
+
loop). Every `sungen` command also prints `Next: sungen next …` as its own last line. A step
|
|
34
|
+
that turns out to be impossible is still a hand-back: say what blocked it, run
|
|
35
|
+
`sungen next <unit>`, and offer what remains.
|
|
30
36
|
|
|
31
37
|
---
|
|
32
38
|
|
|
@@ -719,6 +719,32 @@ missing flow. `sungen audit` then measures `flowCoverage` per DECLARED FLOW and
|
|
|
719
719
|
`FLOW-UNCOVERED` (declared, nobody wrote it), `FLOW-UNDECLARED` (a scenario claiming a flow id the
|
|
720
720
|
inventory never declares), `FLOW-STATUS-UNREASONED` and `FLOW-INVENTORY-MISSING`.
|
|
721
721
|
|
|
722
|
+
**One id per atomic CLAIM in the viewpoint.** The flow id groups; the viewpoint id identifies.
|
|
723
|
+
Writing `FL-BF-001` on four different claims makes the ledger count four items while traceability
|
|
724
|
+
sees one, so the coverage arithmetic and "which test proves this?" are both wrong and neither
|
|
725
|
+
number looks suspicious (`VIEWPOINT-ID-REUSED`). Number them within the flow —
|
|
726
|
+
`FL-BF-001`, `FL-BF-002`, … — and never cite an id no scenario carries (`TRACE-REF-DANGLING`
|
|
727
|
+
checks the ids mentioned in `test-data/*.yaml` too).
|
|
728
|
+
|
|
729
|
+
**Rank the priorities; do not stamp them.** A Priority-Viewpoints table where every row says
|
|
730
|
+
`High` is a formality, not a priority: nothing can be selected for a smoke run and the release
|
|
731
|
+
conversation has no input (`VIEWPOINT-PRIORITY-UNIFORM`). Rank by business impact and risk — the
|
|
732
|
+
basic flow, data integrity and the auth/context guards are High; a recoverable back-navigation or
|
|
733
|
+
a network blip is usually Normal; an exit path a screen suite already owns is Low.
|
|
734
|
+
|
|
735
|
+
**A continuity claim is an EXACTNESS claim.** A title saying values are *unchanged / preserved /
|
|
736
|
+
intact* must be proven with `see [X] text with {{v}}`, never `contains {{v}}`: a substring match
|
|
737
|
+
passes when the UI concatenates, prefixes or truncates — exactly the defect "unchanged" exists to
|
|
738
|
+
catch, so `contains` proves the opposite of the claim (`CLAIM-WEAK`). If the screen genuinely
|
|
739
|
+
renders extra text around the value, change the title instead of weakening the oracle.
|
|
740
|
+
|
|
741
|
+
**`status: covered` is about the DESIGN, not the evidence.** A flow whose only scenarios are
|
|
742
|
+
`@manual` is accounted for and still has nothing that runs: a regression suite would not notice if
|
|
743
|
+
it broke. The audit reports `FLOW-MANUAL-ONLY` and credits those flows at half in `flowCoverage`,
|
|
744
|
+
so do not treat a manual procedure as equivalent to automation. Check the capability plan for a
|
|
745
|
+
driver that would automate it; if none can, say so plainly in the delivery rather than letting
|
|
746
|
+
`covered` imply a passing test.
|
|
747
|
+
|
|
722
748
|
**One flow, one id — a viewpoint is not a flow.** Several scenarios may prove one flow: give them
|
|
723
749
|
the same flow id and different sequence numbers (`VP-VAL-EF01-001`, `VP-VAL-EF01-002`), never a
|
|
724
750
|
fresh flow id per assertion. Two shapes the audit reports as `FLOW-PHASE-MISFILED`: an `EF` that
|