@sun-asterisk/sungen 3.2.24-beta.2 → 3.2.24-beta.3
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 +2 -13
- 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 +9 -1
- 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/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/orchestrator/templates/ai-src/commands/create-test.md +14 -6
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +6 -0
- package/package.json +3 -3
- package/src/cli/commands/audit.ts +2 -11
- 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 +9 -1
- 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/next-step.ts +237 -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
|
@@ -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
|
+
}
|
|
@@ -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
|
|