@sun-asterisk/sungen 3.3.0-alpha.1 → 3.3.0-alpha.2
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/capabilities/discover.d.ts +4 -0
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +16 -2
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/audit.js +1 -1
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/contract-derive-command.d.ts +3 -2
- package/dist/cli/commands/contract-derive-command.d.ts.map +1 -1
- package/dist/cli/commands/contract-derive-command.js +3 -3
- package/dist/cli/commands/contract-derive-command.js.map +1 -1
- package/dist/cli/commands/contract.js +1 -1
- package/dist/cli/commands/contract.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +9 -2
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/journey.js +2 -2
- package/dist/cli/commands/journey.js.map +1 -1
- package/dist/cli/commands/next.d.ts.map +1 -1
- package/dist/cli/commands/next.js +8 -6
- package/dist/cli/commands/next.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/project-sungen-invocation.d.ts +7 -0
- package/dist/cli/project-sungen-invocation.d.ts.map +1 -0
- package/dist/cli/project-sungen-invocation.js +76 -0
- package/dist/cli/project-sungen-invocation.js.map +1 -0
- package/dist/contract/derive-free-feature.d.ts +8 -0
- package/dist/contract/derive-free-feature.d.ts.map +1 -1
- package/dist/contract/derive-free-feature.js +10 -1
- package/dist/contract/derive-free-feature.js.map +1 -1
- package/dist/contract/trace/collect-trace-evidence.js +1 -1
- package/dist/contract/trace/collect-trace-evidence.js.map +1 -1
- package/dist/contract/trace/test-step-join.d.ts +3 -3
- package/dist/contract/trace/test-step-join.d.ts.map +1 -1
- package/dist/contract/trace/test-step-join.js +2 -2
- package/dist/contract/trace/test-step-join.js.map +1 -1
- package/dist/contract/trace/trace-file-reader.d.ts +7 -1
- package/dist/contract/trace/trace-file-reader.d.ts.map +1 -1
- package/dist/contract/trace/trace-file-reader.js +10 -4
- package/dist/contract/trace/trace-file-reader.js.map +1 -1
- package/dist/contract/trace/trace-run-record.d.ts +4 -1
- package/dist/contract/trace/trace-run-record.d.ts.map +1 -1
- package/dist/contract/trace/trace-run-record.js +20 -14
- package/dist/contract/trace/trace-run-record.js.map +1 -1
- package/dist/contract/trace/trace-steps.js +1 -1
- package/dist/contract/trace/trace-steps.js.map +1 -1
- package/dist/contract/trace/trace-zip-limits.d.ts +5 -1
- package/dist/contract/trace/trace-zip-limits.d.ts.map +1 -1
- package/dist/contract/trace/trace-zip-limits.js +6 -2
- package/dist/contract/trace/trace-zip-limits.js.map +1 -1
- package/dist/decisions/confirmed-decision-commands.d.ts +11 -0
- package/dist/decisions/confirmed-decision-commands.d.ts.map +1 -1
- package/dist/decisions/confirmed-decision-commands.js +21 -3
- package/dist/decisions/confirmed-decision-commands.js.map +1 -1
- package/dist/decisions/human-confirmation.d.ts +2 -0
- package/dist/decisions/human-confirmation.d.ts.map +1 -1
- package/dist/decisions/human-confirmation.js +6 -0
- package/dist/decisions/human-confirmation.js.map +1 -1
- package/dist/exporters/matrix/xlsx-cells.js +1 -1
- package/dist/exporters/matrix/xlsx-cells.js.map +1 -1
- package/dist/graph/evaluate-graph.d.ts +3 -1
- package/dist/graph/evaluate-graph.d.ts.map +1 -1
- package/dist/graph/evaluate-graph.js +8 -5
- package/dist/graph/evaluate-graph.js.map +1 -1
- package/dist/graph/node-decl.d.ts +7 -0
- package/dist/graph/node-decl.d.ts.map +1 -1
- package/dist/graph/node-decl.js.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts +3 -1
- package/dist/graph/nodes/test-case-contract-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/test-case-contract-nodes.js +7 -4
- package/dist/graph/nodes/test-case-contract-nodes.js.map +1 -1
- package/dist/graph/nodes/unit-contract-node.d.ts +2 -1
- package/dist/graph/nodes/unit-contract-node.d.ts.map +1 -1
- package/dist/graph/nodes/unit-contract-node.js +8 -1
- package/dist/graph/nodes/unit-contract-node.js.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.d.ts.map +1 -1
- package/dist/graph/nodes/unit-design-nodes.js +2 -1
- package/dist/graph/nodes/unit-design-nodes.js.map +1 -1
- package/dist/harness/catalog/gate-rules.yaml +2 -2
- package/dist/harness/gates/audit-report-verification.d.ts +15 -6
- package/dist/harness/gates/audit-report-verification.d.ts.map +1 -1
- package/dist/harness/gates/audit-report-verification.js +49 -20
- package/dist/harness/gates/audit-report-verification.js.map +1 -1
- package/dist/harness/gates/delivery-checklist.js +2 -2
- package/dist/harness/gates/delivery-checklist.js.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.d.ts +4 -0
- package/dist/harness/gates/delivery-decision-gate.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-gate.js +26 -1
- package/dist/harness/gates/delivery-decision-gate.js.map +1 -1
- package/dist/harness/gates/delivery-decision-step.d.ts +4 -1
- package/dist/harness/gates/delivery-decision-step.d.ts.map +1 -1
- package/dist/harness/gates/delivery-decision-step.js +17 -2
- package/dist/harness/gates/delivery-decision-step.js.map +1 -1
- package/dist/harness/journey.d.ts +19 -0
- package/dist/harness/journey.d.ts.map +1 -1
- package/dist/harness/journey.js +94 -11
- package/dist/harness/journey.js.map +1 -1
- package/dist/harness/provenance.d.ts +29 -0
- package/dist/harness/provenance.d.ts.map +1 -1
- package/dist/harness/provenance.js +109 -9
- package/dist/harness/provenance.js.map +1 -1
- package/dist/hooks/guard-protected-paths.d.ts.map +1 -1
- package/dist/hooks/guard-protected-paths.js +3 -2
- package/dist/hooks/guard-protected-paths.js.map +1 -1
- package/dist/hooks/guard-rules.d.ts +1 -1
- package/dist/hooks/guard-tty-spoof-rule.d.ts +1 -1
- package/dist/hooks/guard-tty-spoof-rule.js +2 -2
- package/dist/hooks/guard-tty-spoof-rule.js.map +1 -1
- package/dist/hooks/hand-back-core.d.ts +5 -3
- package/dist/hooks/hand-back-core.d.ts.map +1 -1
- package/dist/hooks/hand-back-core.js +17 -10
- package/dist/hooks/hand-back-core.js.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.d.ts.map +1 -1
- package/dist/hooks/session-start-unit-state-hook.js +5 -3
- package/dist/hooks/session-start-unit-state-hook.js.map +1 -1
- package/dist/orchestrator/assets-drift.d.ts +5 -0
- package/dist/orchestrator/assets-drift.d.ts.map +1 -1
- package/dist/orchestrator/assets-drift.js +12 -3
- package/dist/orchestrator/assets-drift.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +1 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +15 -2
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/contract-derive-command.ts +7 -6
- package/src/cli/commands/contract.ts +1 -1
- package/src/cli/commands/delivery.ts +7 -2
- package/src/cli/commands/journey.ts +4 -4
- package/src/cli/commands/next.ts +8 -6
- package/src/cli/index.ts +1 -1
- package/src/cli/project-sungen-invocation.ts +39 -0
- package/src/contract/derive-free-feature.ts +12 -2
- package/src/contract/trace/collect-trace-evidence.ts +1 -1
- package/src/contract/trace/test-step-join.ts +6 -5
- package/src/contract/trace/trace-file-reader.ts +17 -7
- package/src/contract/trace/trace-run-record.ts +14 -8
- package/src/contract/trace/trace-steps.ts +1 -1
- package/src/contract/trace/trace-zip-limits.ts +6 -2
- package/src/decisions/confirmed-decision-commands.ts +25 -4
- package/src/decisions/human-confirmation.ts +5 -0
- package/src/exporters/matrix/xlsx-cells.ts +1 -1
- package/src/graph/evaluate-graph.ts +9 -5
- package/src/graph/node-decl.ts +7 -0
- package/src/graph/nodes/test-case-contract-nodes.ts +9 -4
- package/src/graph/nodes/unit-contract-node.ts +8 -1
- package/src/graph/nodes/unit-design-nodes.ts +2 -1
- package/src/harness/catalog/gate-rules.yaml +2 -2
- package/src/harness/gates/audit-report-verification.ts +51 -23
- package/src/harness/gates/delivery-checklist.ts +2 -2
- package/src/harness/gates/delivery-decision-gate.ts +29 -1
- package/src/harness/gates/delivery-decision-step.ts +19 -3
- package/src/harness/journey.ts +95 -13
- package/src/harness/provenance.ts +99 -11
- package/src/hooks/guard-protected-paths.ts +3 -2
- package/src/hooks/guard-rules.ts +1 -1
- package/src/hooks/guard-tty-spoof-rule.ts +3 -3
- package/src/hooks/hand-back-core.ts +17 -11
- package/src/hooks/session-start-unit-state-hook.ts +5 -3
- package/src/orchestrator/assets-drift.ts +12 -5
- package/src/orchestrator/project-initializer.ts +1 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
|
@@ -711,6 +711,8 @@ async function exportMatrixTarget(
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
const { model, mapMissing, mapErrors } = loadMatrixModel(paths);
|
|
714
|
+
// An audit that is untrusted or FAILs never yields an official deliverable: --preview renders a DRAFT (#935).
|
|
715
|
+
if (model && decisions?.auditDraft(qaDir(cwd, target))) model.manifest.approvalState = 'draft';
|
|
714
716
|
|
|
715
717
|
if (mapMissing) {
|
|
716
718
|
log(`${COLOR.yellow}${paths.label}: no delivery map${COLOR.reset} (${path.relative(cwd, paths.mapFile)})`);
|
|
@@ -1095,9 +1097,12 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
1095
1097
|
// an approval are the normal state of a live project. Preflight skips are
|
|
1096
1098
|
// not counted here: reaching this point in an explicit scope means
|
|
1097
1099
|
// --continue-on-missing was passed, which IS the opt-in to tolerate them.
|
|
1098
|
-
|
|
1100
|
+
// --check reports like its checklist: an audit box that does not hold is a non-zero exit (#935).
|
|
1101
|
+
const auditFails = (decisions?.untrustedAudits.length ?? 0) + (decisions?.failingAudits.length ?? 0);
|
|
1102
|
+
if (options.check && auditFails) log(`${COLOR.red}✗ --check: ${auditFails} unit(s) whose audit does not pass — see the audit box above.${COLOR.reset}`);
|
|
1103
|
+
const failed = (explicitScope
|
|
1099
1104
|
? notExported.length > 0
|
|
1100
|
-
: matrixSummaries.length === 0;
|
|
1105
|
+
: matrixSummaries.length === 0) || (!!options.check && auditFails > 0);
|
|
1101
1106
|
if (failed) process.exit(1);
|
|
1102
1107
|
return;
|
|
1103
1108
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import { runJourney,
|
|
3
|
+
import { runJourney, renderJourneyBoard, rollupJourney, renderRollupBoard, writeJourneyFileBestEffort } from '../../harness/journey';
|
|
4
4
|
import { reportSlug } from '../../harness/unit-paths';
|
|
5
5
|
import { findUnitDir } from '../resolve-unit';
|
|
6
|
-
import { answerPendingMigration, decideWithConfirmation, DECIDE_KINDS, waiveWithConfirmation } from '../../decisions/confirmed-decision-commands';
|
|
6
|
+
import { answerPendingMigration, decideWithConfirmation, DECIDE_KINDS, signoffWithConfirmation, waiveWithConfirmation } from '../../decisions/confirmed-decision-commands';
|
|
7
7
|
import { processIO } from '../../decisions/human-confirmation';
|
|
8
8
|
|
|
9
9
|
const collect = (value: string, previous: string[]): string[] => [...previous, value];
|
|
@@ -22,7 +22,7 @@ export function registerJourneyCommand(program: Command): void {
|
|
|
22
22
|
.option('--payload <json>', 'Kind-specific fields as a JSON object, e.g. a gate-override\'s {"rule","severity","scope"} (with --decide)')
|
|
23
23
|
.option('--visibility <v>', 'internal (default, never exported) | client (with --decide)')
|
|
24
24
|
.option('--supersedes <id>', 'The decision this one replaces — the old entry is never edited (with --decide)')
|
|
25
|
-
.option('--signoff', 'Sign off the review queue — the single human gate (allowed only when every other obligation is satisfied/waived)')
|
|
25
|
+
.option('--signoff', 'Sign off the review queue — the single human gate (allowed only when every other obligation is satisfied/waived); a human types a confirmation on a TTY')
|
|
26
26
|
.option('--by <name>', 'Who is signing off (recorded with --signoff)')
|
|
27
27
|
.option('--json', 'Output the raw JSON report')
|
|
28
28
|
.action(async (options) => {
|
|
@@ -63,7 +63,7 @@ export function registerJourneyCommand(program: Command): void {
|
|
|
63
63
|
const report = options.waive
|
|
64
64
|
? await waiveWithConfirmation({ projectRoot: process.cwd(), unit: name, obligation: options.waive, reason: options.reason || '', io })
|
|
65
65
|
: options.signoff
|
|
66
|
-
?
|
|
66
|
+
? await signoffWithConfirmation({ projectRoot: process.cwd(), unit: name, by: options.by, io })
|
|
67
67
|
: runJourney(process.cwd(), name);
|
|
68
68
|
|
|
69
69
|
const outDir = path.join(process.cwd(), '.sungen', 'journey');
|
package/src/cli/commands/next.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { testCaseStatusLabel } from '../../graph/test-case-satisfied';
|
|
|
20
20
|
import { testCaseStates } from '../../graph/test-case-states';
|
|
21
21
|
import { deriveSteps, NextStep, readUnitState, UnitState } from '../../harness/next-step';
|
|
22
22
|
import { resolveUnit, resolveUnitCandidates, AmbiguousUnitError } from '../resolve-unit';
|
|
23
|
+
import { withProjectInvocation } from '../project-sungen-invocation';
|
|
23
24
|
|
|
24
25
|
/** Every unit the project holds, so `sungen next` with no argument still answers. */
|
|
25
26
|
function allUnits(cwd: string): Array<{ name: string; kind: UnitState['kind']; dir: string }> {
|
|
@@ -35,15 +36,16 @@ function allUnits(cwd: string): Array<{ name: string; kind: UnitState['kind']; d
|
|
|
35
36
|
return out;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
/** Steps as text, each `sungen …` command as this project runs it (#939); `--json` keeps them canonical. */
|
|
40
|
+
function renderSteps(steps: NextStep[], cwd: string): void {
|
|
39
41
|
for (const st of steps) {
|
|
40
42
|
const mark = st.kind === 'blocked' ? '⛔' : st.kind === 'optional' ? '○' : st.kind === 'decide' ? '⚖' : '→';
|
|
41
|
-
console.log(` ${mark} ${st.command}${st.kind === 'decide' ? ' (a QA decision — ask, do not run)' : ''}`);
|
|
43
|
+
console.log(` ${mark} ${withProjectInvocation(st.command, cwd)}${st.kind === 'decide' ? ' (a QA decision — ask, do not run)' : ''}`);
|
|
42
44
|
console.log(` ${st.because}`);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
function render(s: UnitState): void {
|
|
48
|
+
function render(s: UnitState, cwd: string): void {
|
|
47
49
|
const L = console.log;
|
|
48
50
|
L('');
|
|
49
51
|
L(`━━━ Next steps: ${s.unit} (${s.kind}) ━━━`);
|
|
@@ -59,7 +61,7 @@ function render(s: UnitState): void {
|
|
|
59
61
|
if (s.audit) L(` audit: ${s.audit.overall}/10 [${s.audit.gateStatus}] · ${s.audit.findings.length} finding(s)`);
|
|
60
62
|
else if (s.auditNotCounted) L(` audit: ${s.auditNotCounted.state === 'stale' ? 'stale' : 'not trusted'} — ${s.auditNotCounted.reason}`);
|
|
61
63
|
L('');
|
|
62
|
-
renderSteps(s.steps);
|
|
64
|
+
renderSteps(s.steps, cwd);
|
|
63
65
|
L('');
|
|
64
66
|
}
|
|
65
67
|
|
|
@@ -89,7 +91,7 @@ function caseView(s: UnitState, cwd: string, unitDir: string, id: string, json:
|
|
|
89
91
|
if (tc.anchors.length) L(` anchors: ${tc.anchors.map((a) => `${a.anchor} (${a.change})`).join(', ')}`);
|
|
90
92
|
for (const f of tc.findings) L(` finding: ${f.code} [${f.confidence.label} ${f.confidence.score}] ${f.citation}`);
|
|
91
93
|
L('');
|
|
92
|
-
if (steps.length) renderSteps(steps);
|
|
94
|
+
if (steps.length) renderSteps(steps, cwd);
|
|
93
95
|
else L(` ✓ nothing to do for this test case — ${testCaseStatusLabel(tc)}`);
|
|
94
96
|
L('');
|
|
95
97
|
}
|
|
@@ -131,7 +133,7 @@ export function registerNextCommand(program: Command): void {
|
|
|
131
133
|
console.log(JSON.stringify(states.length === 1 ? states[0] : states, null, 2));
|
|
132
134
|
return;
|
|
133
135
|
}
|
|
134
|
-
for (const s of states) render(s);
|
|
136
|
+
for (const s of states) render(s, cwd);
|
|
135
137
|
} catch (e) {
|
|
136
138
|
console.error(`Error: ${e instanceof Error ? e.message : e}`);
|
|
137
139
|
process.exit(1);
|
package/src/cli/index.ts
CHANGED
|
@@ -56,7 +56,7 @@ async function main() {
|
|
|
56
56
|
// process exit (not postAction) because several commands exit with a MEANINGFUL code that skips
|
|
57
57
|
// commander's hooks (`depth-lint` → 2). It points at `sungen next` instead of restating a step,
|
|
58
58
|
// so it cannot drift from the project state. A failure prints it on stderr after the error;
|
|
59
|
-
// `--json`/`--grep` keep it off stdout (#836); `
|
|
59
|
+
// `--json`/`--grep` keep it off stdout (#836); `hook` never prints one, `next` only when it fails (#937). The line and
|
|
60
60
|
// those rules live in the hook core (src/hooks/hand-back-core.ts) — this file only wires it.
|
|
61
61
|
const handBack = attachCliHandBack(process.argv, process.cwd());
|
|
62
62
|
const program = new Command();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a hint names sungen inside a project (#939, items 1–2).
|
|
3
|
+
*
|
|
4
|
+
* A project that installs its own sungen (`npm i -D @sun-asterisk/sungen`) runs it with `npx sungen`;
|
|
5
|
+
* a bare `sungen` is whatever global install the machine has — in the field test an older 3.2.24-beta
|
|
6
|
+
* beside the project's 3.3.0-alpha. And `sungen update` without a flag switches to the stable
|
|
7
|
+
* channel, so on an alpha or beta install the update hint must keep the channel it runs on.
|
|
8
|
+
*
|
|
9
|
+
* sungenInvocation(root) 'npx sungen' when <root>/node_modules/@sun-asterisk/sungen exists, else 'sungen'
|
|
10
|
+
* withProjectInvocation(cmd, root) a canonical `sungen …` command as this project runs it
|
|
11
|
+
* updateCommand(root, version?) `<invocation> update` plus `--alpha` / `--beta` (`--tag <id>` for another prerelease)
|
|
12
|
+
*
|
|
13
|
+
* Only the project root itself is looked at — where `npm i` puts a QA project's install — so a
|
|
14
|
+
* monorepo checkout above a fixture does not turn its hints into `npx`.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { getPackageVersion } from '../exporters/package-info';
|
|
19
|
+
|
|
20
|
+
export function sungenInvocation(projectRoot: string): string {
|
|
21
|
+
return fs.existsSync(path.join(projectRoot, 'node_modules', '@sun-asterisk', 'sungen', 'package.json')) ? 'npx sungen' : 'sungen';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** `sungen audit --screen x` → `npx sungen audit --screen x` in a project with its own install; anything else unchanged. */
|
|
25
|
+
export function withProjectInvocation(command: string, projectRoot: string): string {
|
|
26
|
+
const inv = sungenInvocation(projectRoot);
|
|
27
|
+
return inv !== 'sungen' && /^sungen\b/.test(command) ? `${inv}${command.slice('sungen'.length)}` : command;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The `sungen update` flag that keeps the channel of `version` (its prerelease id); '' for stable. */
|
|
31
|
+
export function updateChannelFlag(version: string = getPackageVersion()): string {
|
|
32
|
+
const id = version.match(/^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/)?.[1];
|
|
33
|
+
if (!id) return '';
|
|
34
|
+
return id === 'alpha' || id === 'beta' ? ` --${id}` : ` --tag ${id}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function updateCommand(projectRoot: string, version: string = getPackageVersion()): string {
|
|
38
|
+
return `${sungenInvocation(projectRoot)} update${updateChannelFlag(version)}`;
|
|
39
|
+
}
|
|
@@ -35,7 +35,8 @@ export interface DeriveResult {
|
|
|
35
35
|
file: string;
|
|
36
36
|
written: boolean;
|
|
37
37
|
scenarios: Array<{ id: string; title: string; steps: number; low: number; opaque: number }>;
|
|
38
|
-
|
|
38
|
+
/** `approved: true` — skipped because QA already approved it (derive never rewrites one), not for want of a trace. */
|
|
39
|
+
skipped: Array<{ id: string; reason: string; approved?: true }>;
|
|
39
40
|
refused?: SecretHit[];
|
|
40
41
|
note?: string;
|
|
41
42
|
}
|
|
@@ -58,6 +59,15 @@ function semanticHits(text: string, label: string): SecretHit[] {
|
|
|
58
59
|
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Nothing was derived and nothing explains it away: no test of the unit left an admissible trace
|
|
64
|
+
* (#932). Not when the scenarios are approved (derive leaves those alone by design), up to date,
|
|
65
|
+
* refused, or the project is mobile (`note`).
|
|
66
|
+
*/
|
|
67
|
+
export function nothingDerived(r: DeriveResult): boolean {
|
|
68
|
+
return !r.note && !r.refused?.length && r.scenarios.length === 0 && !r.skipped.some((s) => s.approved);
|
|
69
|
+
}
|
|
70
|
+
|
|
61
71
|
export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
|
|
62
72
|
const unitBase = path.basename(i.unit.name);
|
|
63
73
|
const file = path.join(i.unit.dir, 'features', `${unitBase}.free.feature`);
|
|
@@ -79,7 +89,7 @@ export async function deriveFreeFeature(i: DeriveInput): Promise<DeriveResult> {
|
|
|
79
89
|
for (const id of [...byId.keys()].sort((a, b) => a.localeCompare(b, 'en'))) {
|
|
80
90
|
const runs = admissible(byId.get(id)!);
|
|
81
91
|
if (!runs.length) { result.skipped.push({ id, reason: byId.get(id)![0].reason ?? 'no admissible trace' }); continue; }
|
|
82
|
-
if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`' }); continue; }
|
|
92
|
+
if (kept.some((b) => b.id === id && !b.proposed)) { result.skipped.push({ id, reason: 'approved — derive never rewrites an approved scenario; use `sungen contract check --trace`', approved: true }); continue; }
|
|
83
93
|
const secrets = new Set<string>();
|
|
84
94
|
const rendered = runs.map((r) => {
|
|
85
95
|
const steps = traceIntentSteps(r.trace!);
|
|
@@ -34,7 +34,7 @@ export interface ScenarioEvidence { runs: Map<string, TraceRun[]>; problems: str
|
|
|
34
34
|
|
|
35
35
|
/** The reason a run is not judged once the verdict's deadline has passed (fail closed). */
|
|
36
36
|
export const OVER_BUDGET = 'verdict budget exceeded; run `sungen contract check`';
|
|
37
|
-
const NO_RECORD = "no sungen run record under test-results/ — run the free tests with trace: 'on' and the sungen run-record reporter";
|
|
37
|
+
const NO_RECORD = "no sungen run record under test-results-free/ (or test-results/) — run the free tests with their own config: outputDir 'test-results-free', trace: 'on' and the sungen run-record reporter";
|
|
38
38
|
|
|
39
39
|
/** The real path of `file` when it is a regular file INSIDE the project (symlinks resolved); else null. */
|
|
40
40
|
export function insideProject(projectRoot: string, file: string): string | null {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Join the test runner's steps (test.trace) with the browser calls (<N>-trace.trace) — phase 07
|
|
3
|
-
* criterion 2, proven 114/114 on a real app.
|
|
4
|
-
* `callId`;
|
|
3
|
+
* criterion 2, proven 114/114 on a real app. Up to format 9 a library call's `stepId` is the runner
|
|
4
|
+
* step's `callId`; from format 10 (Playwright 1.64) the two share ONE id and `stepId` is gone, so the
|
|
5
|
+
* call's own `callId` is the step. Walking the runner's `parentId` chain says where the call ran:
|
|
5
6
|
*
|
|
6
7
|
* body the test function itself, with the path of enclosing `test.step` titles
|
|
7
8
|
* hook a user `beforeEach` / `afterEach` (stage setup | teardown)
|
|
@@ -26,7 +27,7 @@ export type CallPhase =
|
|
|
26
27
|
| { phase: 'unjoined' };
|
|
27
28
|
|
|
28
29
|
export interface JoinedCall {
|
|
29
|
-
part: string; callId: string;
|
|
30
|
+
part: string; callId: string; class: string; method: string;
|
|
30
31
|
params: Record<string, unknown>; stepId?: string; where: CallPhase; failed: boolean; startTime: number;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -90,9 +91,9 @@ export function joinCalls(trace: ParsedTrace): { steps: Map<string, RunnerStep>;
|
|
|
90
91
|
for (const e of part.events) {
|
|
91
92
|
if (e.type !== 'before') continue;
|
|
92
93
|
const callId = str(e.callId);
|
|
93
|
-
const stepId = str(e.stepId) || undefined;
|
|
94
|
+
const stepId = str(e.stepId) || (part.version >= 10 ? callId : '') || undefined;
|
|
94
95
|
calls.push({
|
|
95
|
-
part: part.name, callId,
|
|
96
|
+
part: part.name, callId, class: str(e.class), method: str(e.method),
|
|
96
97
|
params: (e.params && typeof e.params === 'object' ? e.params : {}) as Record<string, unknown>,
|
|
97
98
|
stepId, where: stepId ? classifyStep(steps, stepId) : { phase: 'unjoined' }, failed: failed.has(callId), startTime: Number(e.startTime) || 0,
|
|
98
99
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* test.trace the test runner's steps (test.step, hooks, fixtures, expect, pw:api)
|
|
6
6
|
* <N>-trace.trace a library part: the browser calls, frame snapshots, logs
|
|
7
7
|
* <N>-trace.network resources (stylesheets inlined into a replayed snapshot come from here)
|
|
8
|
-
* <N>-trace.stacks call
|
|
8
|
+
* <N>-trace.stacks call id → source frames (`file:line` of an opaque step)
|
|
9
9
|
* resources/<sha1> resource bodies
|
|
10
10
|
*
|
|
11
11
|
* Every part that has a header (test.trace and each library part) must declare a supported version;
|
|
@@ -18,7 +18,12 @@ import { SUPPORTED_TRACE_VERSIONS, traceZipLimits, TraceRejected, TraceZipLimits
|
|
|
18
18
|
|
|
19
19
|
export type TraceEvent = { type: string } & Record<string, unknown>;
|
|
20
20
|
export interface StackFrame { file: string; line: number; column: number }
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* One library part. `version` is its declared format (already accepted by the gate); `stacks` is
|
|
23
|
+
* keyed by call id — v8/v9 wrote a call NUMBER (`11` → `call@11`, Playwright's legacy id), v10 the
|
|
24
|
+
* call id itself, which it shares with the runner step (`ulgt@50`).
|
|
25
|
+
*/
|
|
26
|
+
export interface LibraryPart { name: string; version: number; events: TraceEvent[]; network: TraceEvent[]; stacks: Map<string, StackFrame[]> }
|
|
22
27
|
|
|
23
28
|
export interface ParsedTrace {
|
|
24
29
|
label: string;
|
|
@@ -56,19 +61,21 @@ function parseJsonl(zip: ZipArchive, name: string): TraceEvent[] {
|
|
|
56
61
|
return out;
|
|
57
62
|
}
|
|
58
63
|
|
|
59
|
-
function parseStacks(zip: ZipArchive, name: string): Map<
|
|
60
|
-
const out = new Map<
|
|
64
|
+
function parseStacks(zip: ZipArchive, name: string): Map<string, StackFrame[]> {
|
|
65
|
+
const out = new Map<string, StackFrame[]>();
|
|
61
66
|
if (!zip.has(name)) return out;
|
|
62
67
|
let doc: { files?: unknown; stacks?: unknown };
|
|
63
68
|
try { doc = JSON.parse(zip.readText(name)); } catch { throw new TraceRejected('trace.malformed', `${name} is not JSON`); }
|
|
64
69
|
const files = Array.isArray(doc.files) ? doc.files.map(String) : [];
|
|
65
70
|
for (const row of Array.isArray(doc.stacks) ? doc.stacks : []) {
|
|
66
|
-
if (!Array.isArray(row) ||
|
|
71
|
+
if (!Array.isArray(row) || !Array.isArray(row[1])) continue;
|
|
72
|
+
const id = typeof row[0] === 'number' ? `call@${row[0]}` : row[0];
|
|
73
|
+
if (typeof id !== 'string' || !CALL_ID.test(id)) continue;
|
|
67
74
|
const frames = (row[1] as unknown[]).filter(Array.isArray).map((f) => {
|
|
68
75
|
const [fileIdx, line, column] = f as number[];
|
|
69
76
|
return { file: files[fileIdx] ?? '', line: Number(line) || 0, column: Number(column) || 0 };
|
|
70
77
|
});
|
|
71
|
-
out.set(
|
|
78
|
+
out.set(id, frames);
|
|
72
79
|
}
|
|
73
80
|
return out;
|
|
74
81
|
}
|
|
@@ -81,9 +88,12 @@ export function readTraceBuffer(buf: Buffer, label: string, limits: TraceZipLimi
|
|
|
81
88
|
const runner = parseJsonl(zip, 'test.trace');
|
|
82
89
|
const library: LibraryPart[] = zip.names().filter((n) => /^\d+-trace\.trace$/.test(n)).sort().map((n) => {
|
|
83
90
|
const base = n.replace(/\.trace$/, '');
|
|
91
|
+
const events = parseJsonl(zip, n);
|
|
92
|
+
const version = headerOf(events)?.version;
|
|
84
93
|
return {
|
|
85
94
|
name: n,
|
|
86
|
-
|
|
95
|
+
version: typeof version === 'number' ? version : NaN, // a non-number is refused just below
|
|
96
|
+
events,
|
|
87
97
|
network: zip.has(`${base}.network`) ? parseJsonl(zip, `${base}.network`) : [],
|
|
88
98
|
stacks: parseStacks(zip, `${base}.stacks`),
|
|
89
99
|
};
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* the code that produced it live, so an unknown key, a wrong type or a malformed hash means the
|
|
5
5
|
* record is not trusted at all (every trace it would vouch for is unbound).
|
|
6
6
|
*
|
|
7
|
-
* Found by convention: `<project>/test-results/sungen-run-record*.json`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Found by convention: `<project>/test-results-free/sungen-run-record*.json` — the free-test config's
|
|
8
|
+
* own outputDir (#941) — or `<project>/test-results/sungen-run-record*.json` (one per run or shard).
|
|
9
|
+
* Playwright wipes its outputDir at the start of every run, so a record and its traces share a
|
|
10
|
+
* lifetime; the free run has its own so the compiled suite's next run cannot delete its traces.
|
|
10
11
|
*/
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
@@ -31,13 +32,18 @@ const ATTEMPT = new Set(['testId', 'file', 'title', 'retry', 'status', 'startTim
|
|
|
31
32
|
const SHA = /^[0-9a-f]{64}$/;
|
|
32
33
|
const isObj = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
/** The free / impl:code run's own outputDir (the documented free-test config); compiled runs clear `test-results/`. */
|
|
36
|
+
export const FREE_RUN_DIR = 'test-results-free';
|
|
37
|
+
/** Where run records are read, in this order: the free run's dir, then Playwright's default (older configs). */
|
|
38
|
+
export const RUN_RECORD_DIRS: readonly string[] = [FREE_RUN_DIR, 'test-results'];
|
|
35
39
|
|
|
36
40
|
export function findRunRecords(projectRoot: string): string[] {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
return RUN_RECORD_DIRS.flatMap((rel) => {
|
|
42
|
+
const dir = path.join(projectRoot, rel);
|
|
43
|
+
let names: string[];
|
|
44
|
+
try { names = fs.readdirSync(dir); } catch { return []; }
|
|
45
|
+
return names.filter((n) => /^sungen-run-record(?:\.[A-Za-z0-9._-]+)?\.json$/.test(n)).sort().map((n) => path.join(dir, n));
|
|
46
|
+
});
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
function attemptOf(raw: unknown, i: number): RunRecordAttempt {
|
|
@@ -70,7 +70,7 @@ export function traceIntentSteps(trace: ParsedTrace): IntentStep[] {
|
|
|
70
70
|
const runner = c.stepId ? steps.get(c.stepId) : undefined;
|
|
71
71
|
out.push({
|
|
72
72
|
kind, method: c.method, callId: c.callId, params: c.params, group: groupOf(steps, c.stepId),
|
|
73
|
-
location: firstFrame(runner?.stack) ?? firstFrame(stacks.get(c.part)?.get(c.
|
|
73
|
+
location: firstFrame(runner?.stack) ?? firstFrame(stacks.get(c.part)?.get(c.callId)),
|
|
74
74
|
...(verbatim ? { verbatim } : {}), startTime: c.startTime,
|
|
75
75
|
});
|
|
76
76
|
}
|
|
@@ -50,8 +50,12 @@ export function traceZipLimits(env: Record<string, string | undefined> = process
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Trace format versions this reader was verified against: 8 = Playwright 1.57, 9 = 1.63, 10 = 1.64
|
|
55
|
+
* (one shared call id for a library call and its runner step; stacks keyed by that id). Anything
|
|
56
|
+
* else — a newer format included — is refused until it has been checked the same way.
|
|
57
|
+
*/
|
|
58
|
+
export const SUPPORTED_TRACE_VERSIONS: ReadonlySet<number> = new Set([8, 9, 10]);
|
|
55
59
|
|
|
56
60
|
export type TraceRejectCode =
|
|
57
61
|
| 'zip.too-large' | 'zip.too-many-entries' | 'zip.entry-too-large' | 'zip.total-too-large' | 'zip.ratio'
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* CLI runs on process.stdin/stderr is the code a test drives with a simulated TTY.
|
|
4
4
|
*
|
|
5
5
|
* waive `sungen journey --screen <u> --waive <OB> --reason …` phrase: the obligation id
|
|
6
|
+
* signoff `sungen journey --screen <u> --signoff [--by <name>]` phrase: SIGN-OFF (review W3)
|
|
6
7
|
* decide `sungen journey [--screen <u>] --decide <kind> --text … --reason … [--anchor …]` phrase: the kind
|
|
7
8
|
* migration a leftover write-ahead sentinel → REVERT or KEEP (takumi T6)
|
|
8
9
|
*
|
|
@@ -16,13 +17,13 @@
|
|
|
16
17
|
import * as path from 'path';
|
|
17
18
|
import { assertNoPendingMigration, pendingMigrationState, PendingMigration, readPendingMigration, resolvePendingMigration, sentinelPath } from '../basis/migration-sentinel';
|
|
18
19
|
import { parseAnchorId } from '../basis/anchor-grammar';
|
|
19
|
-
import { JourneyReport, runJourney, waive } from '../harness/journey';
|
|
20
|
+
import { assertCanSignOff, assertWaivable, JourneyReport, runJourney, signoff, signoffSigner, waive } from '../harness/journey';
|
|
20
21
|
import { reportSlug } from '../harness/unit-paths';
|
|
21
22
|
import { appendDecision, AppendResult } from './append-decision';
|
|
22
23
|
import { PROJECT_SCOPE, unitDecisionScope } from './decision-file';
|
|
23
24
|
import { DecisionDraft, DecisionKind, draftErrors, JsonValue } from './decision-schema';
|
|
24
25
|
import { gitUserName } from './decision-source';
|
|
25
|
-
import { askMigrationChoice, ConfirmationIO, formatCommand, requireTypedConfirmation } from './human-confirmation';
|
|
26
|
+
import { askMigrationChoice, ConfirmationIO, formatCommand, requireInteractive, requireTypedConfirmation } from './human-confirmation';
|
|
26
27
|
import { readDecisions } from './read-decisions';
|
|
27
28
|
import { overridePayloadErrors } from '../harness/gates/gate-override-validation';
|
|
28
29
|
|
|
@@ -30,8 +31,7 @@ export interface WaiveInput { projectRoot: string; unit: string; obligation: str
|
|
|
30
31
|
|
|
31
32
|
export async function waiveWithConfirmation(i: WaiveInput): Promise<JourneyReport> {
|
|
32
33
|
if (!i.reason || !i.reason.trim()) throw new Error('A reason is required to waive (anti-amnesia: a waiver must record WHY). Use --reason "...".');
|
|
33
|
-
|
|
34
|
-
if (!valid.includes(i.obligation)) throw new Error(`Unknown obligation "${i.obligation}". Valid: ${valid.join(', ')}`);
|
|
34
|
+
assertWaivable(runJourney(i.projectRoot, i.unit), i.unit, i.obligation);
|
|
35
35
|
await requireTypedConfirmation({
|
|
36
36
|
what: `waive ${i.obligation} for ${i.unit}`,
|
|
37
37
|
phrase: i.obligation,
|
|
@@ -42,6 +42,27 @@ export async function waiveWithConfirmation(i: WaiveInput): Promise<JourneyRepor
|
|
|
42
42
|
return waive(i.projectRoot, i.unit, i.obligation, i.reason);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface SignoffInput { projectRoot: string; unit: string; by?: string; io: ConfirmationIO }
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The single human gate (DOS §3.4, S5), typed by a person on a terminal: the gate suggests it, so an
|
|
49
|
+
* agent must not be able to give it. Refused before the prompt when the board cannot be signed off.
|
|
50
|
+
*/
|
|
51
|
+
export async function signoffWithConfirmation(i: SignoffInput): Promise<JourneyReport> {
|
|
52
|
+
const what = `sign off ${i.unit} (the single human gate before delivery)`;
|
|
53
|
+
const command = formatCommand(['sungen', 'journey', '--screen', i.unit, '--signoff', ...(i.by ? ['--by', i.by] : [])]);
|
|
54
|
+
requireInteractive(i.io, what, command); // before the board is read: a refused sign-off writes nothing
|
|
55
|
+
const { signer, gitUser } = signoffSigner(i.projectRoot, i.unit, i.by); // nobody to name → refused, no prompt
|
|
56
|
+
const before = runJourney(i.projectRoot, i.unit);
|
|
57
|
+
assertCanSignOff(before);
|
|
58
|
+
await requireTypedConfirmation({
|
|
59
|
+
what, phrase: 'SIGN-OFF', command,
|
|
60
|
+
summary: [`by: ${signer}${signer !== gitUser ? ` (recorded by git user ${gitUser})` : ''}`, `${before.needsYou.length} item(s) in the review queue — you have reviewed them`, 'it goes stale as soon as the audit changes (sign off again then)'],
|
|
61
|
+
io: i.io,
|
|
62
|
+
});
|
|
63
|
+
return signoff(i.projectRoot, i.unit, i.by);
|
|
64
|
+
}
|
|
65
|
+
|
|
45
66
|
/** Kinds `--decide` records. The others have a command of their own that computes their payload. */
|
|
46
67
|
export const DECIDE_KINDS: readonly DecisionKind[] = ['gate-override', 'clarification'];
|
|
47
68
|
const OWN_COMMAND: Partial<Record<DecisionKind, string>> = {
|
|
@@ -79,6 +79,11 @@ function refuseWithoutTty(what: string, command: string): HumanConfirmationRefus
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Refuse up front, before any state is read or written, when no human is at a terminal. */
|
|
83
|
+
export function requireInteractive(io: ConfirmationIO, what: string, command: string): void {
|
|
84
|
+
if (!isInteractive(io)) throw refuseWithoutTty(what, command);
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
/** A fresh, hard-to-guess suffix for one prompt. */
|
|
83
88
|
export const confirmationNonce = (): string => randomBytes(3).toString('hex').toUpperCase();
|
|
84
89
|
|
|
@@ -49,7 +49,7 @@ export function draftBanner(ws: ExcelJS.Worksheet, model: MatrixModel): void {
|
|
|
49
49
|
if (model.manifest.approvalState !== 'draft') return;
|
|
50
50
|
ws.mergeCells('A5:H5');
|
|
51
51
|
const c = ws.getCell('A5');
|
|
52
|
-
c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings
|
|
52
|
+
c.value = 'DRAFT — NOT APPROVED FOR OFFICIAL DELIVERY (review-required findings, proposed groups, or an audit that does not pass)';
|
|
53
53
|
c.font = { bold: true, size: 11, name: ARIAL_FONT, color: { argb: 'FFFFFFFF' } };
|
|
54
54
|
c.fill = { type: 'pattern', pattern: 'solid', fgColor: DRAFT_RED };
|
|
55
55
|
c.alignment = { horizontal: 'center', vertical: 'middle' };
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* open active ∧ ¬satisfied (a test-case node is open when at least one case is)
|
|
6
6
|
* frontier open ∧ (every `requires` satisfied ∨ ¬blocking)
|
|
7
|
-
* cut an open blocking node ends the walk — nothing ranked after it is offered
|
|
7
|
+
* cut an open blocking node ends the walk — nothing ranked after it is offered, except a
|
|
8
|
+
* node's instances that `surfacesPastCut` (a hard contract verdict, #940), which follow
|
|
9
|
+
* the cutting step and never cut themselves
|
|
8
10
|
*
|
|
9
11
|
* `--case <id>` (`testCase` option): the frontier is that case's test-case nodes plus the blocking
|
|
10
12
|
* unit nodes before them; non-blocking unit advice is left to the unit view. A `caseOnly` test-case
|
|
@@ -41,22 +43,24 @@ export function evaluateGraph(
|
|
|
41
43
|
const ordered = [...nodes].sort((a, b) => a.rank - b.rank);
|
|
42
44
|
const open = new Set<string>();
|
|
43
45
|
const out: NextStep[] = [];
|
|
46
|
+
let cut = false;
|
|
44
47
|
for (const node of ordered) {
|
|
45
48
|
if (!node.appliesTo.includes(s.kind) || (node.granularity === 'test-case' && node.caseOnly && !opts.testCase)) continue;
|
|
49
|
+
if (cut && !node.surfacesPastCut) continue;
|
|
46
50
|
let step: NextStep | null = null;
|
|
47
51
|
if (node.granularity === 'unit') {
|
|
48
|
-
if (!node.active(s) || node.satisfied(s)) continue;
|
|
52
|
+
if (!node.active(s) || node.satisfied(s) || (cut && !node.surfacesPastCut!(s))) continue;
|
|
49
53
|
if (!opts.testCase || node.blocking) step = withMeta(node, node.step(s));
|
|
50
54
|
} else {
|
|
51
|
-
const cases = openCases(node, s, tcStates, opts.testCase);
|
|
55
|
+
const cases = openCases(node, s, tcStates, opts.testCase).filter((tc) => !cut || node.surfacesPastCut!(s, tc));
|
|
52
56
|
if (!cases.length) continue;
|
|
53
57
|
const single = Boolean(opts.testCase);
|
|
54
58
|
step = withMeta(node, node.step(s, cases, single), single ? cases[0].id : undefined);
|
|
55
59
|
}
|
|
56
60
|
open.add(node.id);
|
|
57
61
|
const ready = node.requires.every((r) => !open.has(r));
|
|
58
|
-
if (step && (ready || !node.blocking)) out.push(step);
|
|
59
|
-
if (node.blocking)
|
|
62
|
+
if (step && (cut || ready || !node.blocking)) out.push(step);
|
|
63
|
+
if (node.blocking && !cut) cut = true;
|
|
60
64
|
}
|
|
61
65
|
return out;
|
|
62
66
|
}
|
package/src/graph/node-decl.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface UnitNodeDecl extends NodeDeclBase {
|
|
|
42
42
|
active(s: UnitState): boolean;
|
|
43
43
|
satisfied(s: UnitState): boolean;
|
|
44
44
|
step(s: UnitState): StepCore;
|
|
45
|
+
/**
|
|
46
|
+
* Offered even when a blocking node ranked before it cut the walk — only while this holds (#940: a
|
|
47
|
+
* hard contract verdict must not hide behind a stale or failing audit). It never cuts by itself.
|
|
48
|
+
*/
|
|
49
|
+
surfacesPastCut?(s: UnitState): boolean;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export interface TestCaseNodeDecl extends NodeDeclBase {
|
|
@@ -55,6 +60,8 @@ export interface TestCaseNodeDecl extends NodeDeclBase {
|
|
|
55
60
|
satisfied(s: UnitState, tc: TestCaseState): boolean;
|
|
56
61
|
/** One step for `cases` (all open at this node): a unit-level fold, or exactly one case when `single`. */
|
|
57
62
|
step(s: UnitState, cases: TestCaseState[], single: boolean): StepCore;
|
|
63
|
+
/** The cases offered even past a cut (see UnitNodeDecl.surfacesPastCut). */
|
|
64
|
+
surfacesPastCut?(s: UnitState, tc: TestCaseState): boolean;
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
export type NodeDecl = UnitNodeDecl | TestCaseNodeDecl;
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* tc.conformance its trace is in review: a `low` target identity do contract check --trace
|
|
12
12
|
* · its final attempt did not pass (inconclusive) do run-test --ids
|
|
13
13
|
* · a step no code can compare (opaque) decide contract check
|
|
14
|
-
*
|
|
14
|
+
* tc.contract-check (a weaker / retargeted / lost check, or an uncommitted lock entry) and tc.unproven
|
|
15
|
+
* are hard verdicts: they are offered past a stale or failing audit (the cut at unit.audit-stale /
|
|
16
|
+
* unit.gate-fail), #940; tc.contract-approve (changed, not weaker) is review and waits behind it. Exempt cases (DOS-11.2) open nothing. A proposed case (never approved) asks nothing of its own: a
|
|
15
17
|
* compiled test without a lock is never blocked, and the unit view offers the approval.
|
|
16
18
|
*/
|
|
17
19
|
import { ALL_KINDS, StepCore, TestCaseNodeDecl } from '../node-decl';
|
|
@@ -29,13 +31,14 @@ const withDetail = (cases: TestCaseState[]): string =>
|
|
|
29
31
|
const check = (s: UnitState, trace = false): string => `sungen contract check --screen ${s.unit}${trace ? ' --trace' : ''}`;
|
|
30
32
|
|
|
31
33
|
function contractNode(id: string, rank: number, title: string, actor: TestCaseNodeDecl['actor'], gates: string[], caseOnly: boolean,
|
|
32
|
-
active: (tc: TestCaseState) => boolean, step: Step): TestCaseNodeDecl {
|
|
34
|
+
active: (tc: TestCaseState) => boolean, step: Step, hard?: (tc: TestCaseState) => boolean): TestCaseNodeDecl {
|
|
33
35
|
return {
|
|
34
36
|
id, title, actor, granularity: 'test-case', appliesTo: ALL_KINDS, blocking: false, rank, gates, step,
|
|
35
37
|
requires: caseOnly ? ['unit.gate-fail'] : ['unit.run'], produces: ['contracts.lock.yaml', 'decisions/<id>.yaml'],
|
|
36
38
|
...(caseOnly ? { caseOnly: true as const } : {}),
|
|
37
39
|
active: (_s, tc) => active(tc),
|
|
38
40
|
satisfied: (_s, tc) => exemptionOf(tc) !== null,
|
|
41
|
+
...(hard ? { surfacesPastCut: (_s: UnitState, tc: TestCaseState) => hard(tc) } : {}),
|
|
39
42
|
};
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -60,10 +63,12 @@ export const TEST_CASE_CONTRACT_NODES: readonly TestCaseNodeDecl[] = [
|
|
|
60
63
|
(tc) => Boolean(tc.contract?.uncommitted) || HARD_LADDER.has(classOf(tc) ?? ''),
|
|
61
64
|
(s, [c]) => ({ kind: 'blocked', command: check(s), because: c.contract?.uncommitted
|
|
62
65
|
? `its lock entry is not what QA approved and committed (contract.lock-uncommitted) — \`sungen next ${s.unit}\` names the repair; \`sungen delivery\` is blocked until then`
|
|
63
|
-
: `this case breaks its approved contract (${c.contract?.class}${c.contract?.detail ? `: ${c.contract.detail}` : ''}) — restore the approved check (in the .feature, or in the hand-written test of a free / impl:code case), or a QA approves the change on a terminal (\`sungen contract approve --screen ${s.unit} --scenario ${c.id}\`). \`sungen delivery\` is blocked until then; never weaken an expected value to go green` })
|
|
66
|
+
: `this case breaks its approved contract (${c.contract?.class}${c.contract?.detail ? `: ${c.contract.detail}` : ''}) — restore the approved check (in the .feature, or in the hand-written test of a free / impl:code case), or a QA approves the change on a terminal (\`sungen contract approve --screen ${s.unit} --scenario ${c.id}\`). \`sungen delivery\` is blocked until then; never weaken an expected value to go green` }),
|
|
67
|
+
(tc) => Boolean(tc.contract?.uncommitted) || HARD_LADDER.has(classOf(tc) ?? '')),
|
|
64
68
|
contractNode('tc.unproven', 136, 'Prove the approved free cases with a trace', 'ai', ['contract.unproven'], false,
|
|
65
69
|
(tc) => classOf(tc) === 'unproven',
|
|
66
|
-
(s, cases) => ({ kind: 'do', command: `/sungen:run-test ${s.unit}${idsArg(cases)}`, because: `${cases.length} approved free / impl:code test case(s) have no trace that proves them (${withDetail(cases)}) — run them again with trace: 'on' and the sungen run-record reporter (run-test Phase 4 does), then \`${check(s, true)}\`. Until then \`sungen contract check\` fails (contract.unproven) and \`sungen delivery\` is blocked; re-approving or editing the .feature cannot prove them` })
|
|
70
|
+
(s, cases) => ({ kind: 'do', command: `/sungen:run-test ${s.unit}${idsArg(cases)}`, because: `${cases.length} approved free / impl:code test case(s) have no trace that proves them (${withDetail(cases)}) — run them again with trace: 'on' and the sungen run-record reporter (run-test Phase 4 does), then \`${check(s, true)}\`. Until then \`sungen contract check\` fails (contract.unproven) and \`sungen delivery\` is blocked; re-approving or editing the .feature cannot prove them` }),
|
|
71
|
+
(tc) => classOf(tc) === 'unproven'),
|
|
67
72
|
contractNode('tc.conformance', 138, 'Settle the traces in review', 'ai', ['contract.opaque-step', 'contract.inconclusive'], false,
|
|
68
73
|
(tc) => classOf(tc) === 'opaque' || classOf(tc) === 'inconclusive', conformanceStep),
|
|
69
74
|
];
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* verdict `sungen contract check` and the delivery guard rebuild (buildContractVerdict), so the three
|
|
4
4
|
* never disagree. Always `decide`: an agent presents it and never approves its own edit.
|
|
5
5
|
*
|
|
6
|
-
* unit.contract-approve (rank 105, after the audit gate, before the first run
|
|
6
|
+
* unit.contract-approve (rank 105, after the audit gate, before the first run; a hard verdict —
|
|
7
|
+
* hard or uncommitted — is offered past a stale or failing audit too, #940)
|
|
7
8
|
* error the verdict could not be built → check it on a terminal
|
|
8
9
|
* uncommitted by kind: tampered → re-approve or restore · untracked → commit after review ·
|
|
9
10
|
* edited → review and commit, or `git checkout --` the lock · unverified → commit the
|
|
@@ -66,6 +67,12 @@ export const UNIT_CONTRACT_NODES: readonly UnitNodeDecl[] = [
|
|
|
66
67
|
active: (s) => Boolean(s.contract),
|
|
67
68
|
satisfied: nothingToDecide,
|
|
68
69
|
step: contractStep,
|
|
70
|
+
// A hard verdict must not wait behind a re-audit (#940): the audit's PASS says nothing about it —
|
|
71
|
+
// a broken approved check, or a lock that is not what QA approved and committed (contract.lock-
|
|
72
|
+
// uncommitted, hard in `contract check`; the step names that repair first). What stays behind the
|
|
73
|
+
// cut: a changed-not-weaker contract (review — QA re-approves after the design is fixed) and a
|
|
74
|
+
// verdict that could not be built (not a verdict; `contract check` names the error).
|
|
75
|
+
surfacesPastCut: (s) => !s.contract!.error && (s.contract!.uncommitted > 0 || s.contract!.hard > 0),
|
|
69
76
|
},
|
|
70
77
|
{
|
|
71
78
|
id: 'unit.contract-proposed', title: 'Approve the Test Contract', actor: 'human', granularity: 'unit',
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* 6 unit.flow-inventory a flow contract without `flows:` → add it, re-audit
|
|
11
11
|
*/
|
|
12
12
|
import { ALL_KINDS, UnitNodeDecl, unitKindDir } from '../node-decl';
|
|
13
|
+
import { updateChannelFlag } from '../../cli/project-sungen-invocation';
|
|
13
14
|
|
|
14
15
|
const design = (unit: string): string => `/sungen:create-test ${unit}`;
|
|
15
16
|
|
|
@@ -34,7 +35,7 @@ export const UNIT_DESIGN_NODES: readonly UnitNodeDecl[] = [
|
|
|
34
35
|
satisfied: (s) => !s.assetsDrift,
|
|
35
36
|
step: (s) => ({
|
|
36
37
|
kind: 'do',
|
|
37
|
-
command:
|
|
38
|
+
command: `sungen update${updateChannelFlag(s.assetsDrift!.runningVersion)}`, // keep the channel (#939)
|
|
38
39
|
because: `this project's AI commands/skills were written by sungen ${s.assetsDrift!.recordedVersion} and you are running ${s.assetsDrift!.runningVersion} — upgrading the package does NOT refresh them, so any rule added since ${s.assetsDrift!.recordedVersion} is not in effect for the next run`,
|
|
39
40
|
}),
|
|
40
41
|
},
|
|
@@ -535,8 +535,8 @@ rules:
|
|
|
535
535
|
stage: deliver
|
|
536
536
|
severity: error
|
|
537
537
|
kind: soft
|
|
538
|
-
rationale: "Pre-delivery halt: everything automated plus the single human sign-off
|
|
539
|
-
fix: "Close the open obligations
|
|
538
|
+
rationale: "Pre-delivery halt: everything automated must be satisfied or waived with a reason, plus the single human sign-off, which is given (sungen journey --signoff), never waived."
|
|
539
|
+
fix: "Close or waive the open obligations, then a human signs off: sungen journey --screen <unit> --signoff --by <name>."
|
|
540
540
|
since: "<=3.2.34"
|
|
541
541
|
offAllowed: false
|
|
542
542
|
|