@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.
Files changed (67) hide show
  1. package/dist/cli/commands/audit.d.ts.map +1 -1
  2. package/dist/cli/commands/audit.js +2 -13
  3. package/dist/cli/commands/audit.js.map +1 -1
  4. package/dist/cli/commands/capability.d.ts.map +1 -1
  5. package/dist/cli/commands/capability.js +11 -12
  6. package/dist/cli/commands/capability.js.map +1 -1
  7. package/dist/cli/commands/challenge.d.ts.map +1 -1
  8. package/dist/cli/commands/challenge.js +2 -10
  9. package/dist/cli/commands/challenge.js.map +1 -1
  10. package/dist/cli/commands/depth-lint.d.ts.map +1 -1
  11. package/dist/cli/commands/depth-lint.js +2 -12
  12. package/dist/cli/commands/depth-lint.js.map +1 -1
  13. package/dist/cli/commands/gate.d.ts.map +1 -1
  14. package/dist/cli/commands/gate.js +2 -47
  15. package/dist/cli/commands/gate.js.map +1 -1
  16. package/dist/cli/commands/journey.d.ts.map +1 -1
  17. package/dist/cli/commands/journey.js +2 -12
  18. package/dist/cli/commands/journey.js.map +1 -1
  19. package/dist/cli/commands/manifest.d.ts.map +1 -1
  20. package/dist/cli/commands/manifest.js +2 -10
  21. package/dist/cli/commands/manifest.js.map +1 -1
  22. package/dist/cli/commands/next.d.ts +14 -0
  23. package/dist/cli/commands/next.d.ts.map +1 -0
  24. package/dist/cli/commands/next.js +130 -0
  25. package/dist/cli/commands/next.js.map +1 -0
  26. package/dist/cli/index.js +34 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/resolve-unit.d.ts +22 -0
  29. package/dist/cli/resolve-unit.d.ts.map +1 -0
  30. package/dist/cli/resolve-unit.js +101 -0
  31. package/dist/cli/resolve-unit.js.map +1 -0
  32. package/dist/harness/audit.d.ts.map +1 -1
  33. package/dist/harness/audit.js +9 -1
  34. package/dist/harness/audit.js.map +1 -1
  35. package/dist/harness/capability-plan.d.ts +12 -1
  36. package/dist/harness/capability-plan.d.ts.map +1 -1
  37. package/dist/harness/capability-plan.js +16 -2
  38. package/dist/harness/capability-plan.js.map +1 -1
  39. package/dist/harness/capability.d.ts +12 -0
  40. package/dist/harness/capability.d.ts.map +1 -1
  41. package/dist/harness/capability.js +16 -0
  42. package/dist/harness/capability.js.map +1 -1
  43. package/dist/harness/catalog/drivers.yaml +5 -0
  44. package/dist/harness/next-step.d.ts +40 -0
  45. package/dist/harness/next-step.d.ts.map +1 -0
  46. package/dist/harness/next-step.js +242 -0
  47. package/dist/harness/next-step.js.map +1 -0
  48. package/dist/orchestrator/templates/ai-src/commands/create-test.md +14 -6
  49. package/dist/orchestrator/templates/ai-src/commands/run-test.md +6 -0
  50. package/package.json +3 -3
  51. package/src/cli/commands/audit.ts +2 -11
  52. package/src/cli/commands/capability.ts +11 -10
  53. package/src/cli/commands/challenge.ts +2 -8
  54. package/src/cli/commands/depth-lint.ts +2 -10
  55. package/src/cli/commands/gate.ts +2 -10
  56. package/src/cli/commands/journey.ts +2 -10
  57. package/src/cli/commands/manifest.ts +2 -8
  58. package/src/cli/commands/next.ts +96 -0
  59. package/src/cli/index.ts +31 -0
  60. package/src/cli/resolve-unit.ts +72 -0
  61. package/src/harness/audit.ts +9 -1
  62. package/src/harness/capability-plan.ts +23 -3
  63. package/src/harness/capability.ts +22 -0
  64. package/src/harness/catalog/drivers.yaml +5 -0
  65. package/src/harness/next-step.ts +237 -0
  66. package/src/orchestrator/templates/ai-src/commands/create-test.md +14 -6
  67. 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 create-test run is NOT finished when the files are written or the audit prints. The final
13
- action of EVERY run — success, partial, or aborted — is the next-step hand-back
14
- ({{#cap parallel-subagents}}an `AskUserQuestion` offering the next actions{{/cap}}{{^cap parallel-subagents}}a numbered list of next-action choices{{/cap}};
15
- see "Finish always hand the next step back" at the end of this file). Ending with a prose
16
- summary and no choices is a broken run: the operator is left guessing. This holds no matter
17
- how long the generation/repair loop ran.
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