ccqa 1.33.0 → 1.35.0
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/README.md +17 -0
- package/dist/bin/ccqa.mjs +369 -210
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,23 @@ tenant, an account, a role — not an environment: ccqa tracks one
|
|
|
197
197
|
verification environment
|
|
198
198
|
([ADR-0013](./docs/adr/0013-one-verification-environment.md)).
|
|
199
199
|
|
|
200
|
+
## Agent skills
|
|
201
|
+
|
|
202
|
+
`skills/` ships guides that let a coding agent (e.g. Claude Code) drive ccqa
|
|
203
|
+
end-to-end on its own:
|
|
204
|
+
|
|
205
|
+
- **ccqa-record** — create a new test case: pin down the behavior, write the
|
|
206
|
+
spec, choose deterministic or live mode, record, run to green.
|
|
207
|
+
- **ccqa-rerecord** — bring a flagged or failing test case back to green:
|
|
208
|
+
read the hub's finding, decide what went stale, repair that, re-record.
|
|
209
|
+
|
|
210
|
+
Install them with the [skills CLI](https://github.com/vercel-labs/skills)
|
|
211
|
+
into a consuming project (or `-g` for all projects):
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx skills add <this-repo> --skill ccqa-record --skill ccqa-rerecord
|
|
215
|
+
```
|
|
216
|
+
|
|
200
217
|
## Documentation
|
|
201
218
|
|
|
202
219
|
| I want to… | Read |
|
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -89,17 +89,19 @@ function* iterEnvRefNames(value) {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
|
-
* Resolve every `$VAR` / `${VAR}` reference against
|
|
92
|
+
* Resolve every `$VAR` / `${VAR}` reference against `overrides`, then the
|
|
93
|
+
* current process env. `overrides` carries values an invoker injects into a
|
|
94
|
+
* child process (e.g. CCQA_RUN_ID), which beat the parent env there.
|
|
93
95
|
*
|
|
94
96
|
* Missing variables expand to the empty string, mirroring `sh` behaviour.
|
|
95
97
|
* Throwing would force ccqa to be invoked with every var set even for
|
|
96
98
|
* unused blocks, which is more user-hostile than letting the test fail
|
|
97
99
|
* downstream with a clearer message ("login form rejected: empty password").
|
|
98
100
|
*/
|
|
99
|
-
function resolveEnvRefs(value) {
|
|
101
|
+
function resolveEnvRefs(value, overrides = {}) {
|
|
100
102
|
return value.replace(ENV_VAR_RE, (_, braced, plain) => {
|
|
101
103
|
const name = braced ?? plain ?? "";
|
|
102
|
-
return process.env[name] ?? "";
|
|
104
|
+
return overrides[name] ?? process.env[name] ?? "";
|
|
103
105
|
});
|
|
104
106
|
}
|
|
105
107
|
/**
|
|
@@ -408,6 +410,7 @@ function collectIncludedBlockNames(spec) {
|
|
|
408
410
|
const CCQA_DIR = ".ccqa";
|
|
409
411
|
const SPEC_FILE = "spec.yaml";
|
|
410
412
|
const RECORDING_FILE = "ir.json";
|
|
413
|
+
const FAILED_RECORDING_FILE = "ir.failed.json";
|
|
411
414
|
const PERSPECTIVES_FILE = "perspectives.yaml";
|
|
412
415
|
const PERSPECTIVES_MD_FILE = "perspectives.md";
|
|
413
416
|
function getCcqaDir(cwd = process.cwd()) {
|
|
@@ -476,9 +479,22 @@ async function saveRecording(featureName, specName, actions, cwd) {
|
|
|
476
479
|
await mkdir(specDir, { recursive: true });
|
|
477
480
|
const recordingPath = join(specDir, RECORDING_FILE);
|
|
478
481
|
await writeFile(recordingPath, JSON.stringify(actions, null, 2), "utf-8");
|
|
479
|
-
await Promise.all(LEGACY_RECORDING_FILES.map((f) => unlink(join(specDir, f)).catch(() => {})));
|
|
482
|
+
await Promise.all([...LEGACY_RECORDING_FILES, FAILED_RECORDING_FILE].map((f) => unlink(join(specDir, f)).catch(() => {})));
|
|
480
483
|
return recordingPath;
|
|
481
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Persist the actions of a trace that FAILED. They go to a side file so a
|
|
487
|
+
* recording that did not demonstrate the spec can never replace one that
|
|
488
|
+
* did — `ir.json` and the generated code stay whatever they were. The next
|
|
489
|
+
* successful {@link saveRecording} deletes the file.
|
|
490
|
+
*/
|
|
491
|
+
async function saveFailedRecording(featureName, specName, actions, cwd) {
|
|
492
|
+
const specDir = getSpecDir(featureName, specName, cwd);
|
|
493
|
+
await mkdir(specDir, { recursive: true });
|
|
494
|
+
const path = join(specDir, FAILED_RECORDING_FILE);
|
|
495
|
+
await writeFile(path, JSON.stringify(actions, null, 2), "utf-8");
|
|
496
|
+
return path;
|
|
497
|
+
}
|
|
482
498
|
function getBlocksDir(cwd) {
|
|
483
499
|
return join(getCcqaDir(cwd), "blocks");
|
|
484
500
|
}
|
|
@@ -1326,10 +1342,12 @@ function opt(key, value) {
|
|
|
1326
1342
|
* `instruction` / `expected` may *also* contain `${ENV}` refs that
|
|
1327
1343
|
* don't go through include params.
|
|
1328
1344
|
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1345
|
+
* Each ref resolves against `overrides` first, then `process.env` —
|
|
1346
|
+
* `overrides` carries values the invoker injects into the child process,
|
|
1347
|
+
* which beat the parent env there. Only refs that resolve non-empty land in
|
|
1348
|
+
* the map — scrubbing against an empty string would corrupt unrelated empty
|
|
1349
|
+
* strings in the action stream; the rest are returned via `unresolved` so
|
|
1350
|
+
* the caller can warn the user.
|
|
1333
1351
|
*
|
|
1334
1352
|
* Longer values sort first so a `${SHORT}` whose value is a substring of a
|
|
1335
1353
|
* `${LONG}` value doesn't clobber the longer one.
|
|
@@ -1337,7 +1355,7 @@ function opt(key, value) {
|
|
|
1337
1355
|
* `title` is deliberately NOT scanned — it never reaches the recorded action
|
|
1338
1356
|
* stream.
|
|
1339
1357
|
*/
|
|
1340
|
-
function buildSpecEnvScrub(spec, expanded) {
|
|
1358
|
+
function buildSpecEnvScrub(spec, expanded, overrides = {}) {
|
|
1341
1359
|
const refNames = /* @__PURE__ */ new Set();
|
|
1342
1360
|
for (const step of spec.steps) if (isIncludeStep(step)) for (const v of Object.values(step.params ?? {})) collect(v, refNames);
|
|
1343
1361
|
else {
|
|
@@ -1351,7 +1369,7 @@ function buildSpecEnvScrub(spec, expanded) {
|
|
|
1351
1369
|
const map = [];
|
|
1352
1370
|
const unresolved = [];
|
|
1353
1371
|
for (const name of refNames) {
|
|
1354
|
-
const value = process.env[name];
|
|
1372
|
+
const value = overrides[name] ?? process.env[name];
|
|
1355
1373
|
if (typeof value === "string" && value.length > 0) map.push([value, "${" + name + "}"]);
|
|
1356
1374
|
else unresolved.push(name);
|
|
1357
1375
|
}
|
|
@@ -1383,8 +1401,8 @@ const COMMON_PROSE_VALUES = new Set([
|
|
|
1383
1401
|
* command log too, trading that short-value coverage for not building a
|
|
1384
1402
|
* second map.
|
|
1385
1403
|
*/
|
|
1386
|
-
function buildProseEnvScrubMap(spec, expanded) {
|
|
1387
|
-
return buildSpecEnvScrub(spec, expanded).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
|
|
1404
|
+
function buildProseEnvScrubMap(spec, expanded, overrides = {}) {
|
|
1405
|
+
return buildSpecEnvScrub(spec, expanded, overrides).map.filter(([value]) => value.length >= MIN_PROSE_SCRUB_LENGTH && !COMMON_PROSE_VALUES.has(value.toLowerCase()));
|
|
1388
1406
|
}
|
|
1389
1407
|
/**
|
|
1390
1408
|
* Replace every occurrence of an env value with its `${VAR}` placeholder in
|
|
@@ -1524,16 +1542,21 @@ const ENDPOINT_ENV_KEYS = [
|
|
|
1524
1542
|
"CLAUDE_CODE_OAUTH_TOKEN"
|
|
1525
1543
|
];
|
|
1526
1544
|
/**
|
|
1545
|
+
* When both credentials are present the OAuth token wins and the API key is
|
|
1546
|
+
* dropped. Left to the CLI the API key would win, which makes "switch a CI
|
|
1547
|
+
* job to the subscription token" require unwiring the key everywhere; with
|
|
1548
|
+
* this rule, adding the one variable is the whole switch, and removing it is
|
|
1549
|
+
* the whole rollback. The one place the rule lives — both the resolved view
|
|
1550
|
+
* and the env the SDK receives apply it through here.
|
|
1551
|
+
*/
|
|
1552
|
+
function preferOauthToken(env) {
|
|
1553
|
+
if (env["CLAUDE_CODE_OAUTH_TOKEN"]) delete env["ANTHROPIC_API_KEY"];
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1527
1556
|
* Collects the endpoint/auth variables set in the current process environment
|
|
1528
1557
|
* so they can be forwarded, verbatim, to every Claude Code invocation. Returns
|
|
1529
1558
|
* only the keys that are actually set (non-empty), so unset variables never
|
|
1530
|
-
* override the SDK's own defaults.
|
|
1531
|
-
*
|
|
1532
|
-
* When both credentials are present the OAuth token wins and the API key is
|
|
1533
|
-
* not forwarded. Left to the CLI the API key would win, which makes "switch a
|
|
1534
|
-
* CI job to the subscription token" require unwiring the key everywhere; with
|
|
1535
|
-
* the precedence here, adding the one variable is the whole switch, and
|
|
1536
|
-
* removing it is the whole rollback.
|
|
1559
|
+
* override the SDK's own defaults. Credential precedence per preferOauthToken.
|
|
1537
1560
|
*/
|
|
1538
1561
|
function resolveEndpointEnv() {
|
|
1539
1562
|
const endpointEnv = {};
|
|
@@ -1541,7 +1564,7 @@ function resolveEndpointEnv() {
|
|
|
1541
1564
|
const value = process.env[key];
|
|
1542
1565
|
if (value && value.length > 0) endpointEnv[key] = value;
|
|
1543
1566
|
}
|
|
1544
|
-
|
|
1567
|
+
preferOauthToken(endpointEnv);
|
|
1545
1568
|
return endpointEnv;
|
|
1546
1569
|
}
|
|
1547
1570
|
/**
|
|
@@ -1555,6 +1578,30 @@ function withoutEmptyEndpointVars(env) {
|
|
|
1555
1578
|
for (const key of ENDPOINT_ENV_KEYS) if (out[key] === "") delete out[key];
|
|
1556
1579
|
return out;
|
|
1557
1580
|
}
|
|
1581
|
+
/**
|
|
1582
|
+
* The environment actually handed to the Claude Code process: the full process
|
|
1583
|
+
* environment with the caller's overrides on top, empty endpoint variables
|
|
1584
|
+
* dropped, and — when both credentials survive the merge — the API key removed
|
|
1585
|
+
* so the OAuth token wins.
|
|
1586
|
+
*
|
|
1587
|
+
* That removal MUST happen on the env the SDK receives, not only on the
|
|
1588
|
+
* resolved view: left to the CLI the API key would win, silently moving every
|
|
1589
|
+
* call from the subscription to metered billing when a CI job wires both
|
|
1590
|
+
* (which is exactly what happened before this function existed).
|
|
1591
|
+
*
|
|
1592
|
+
* Returns undefined when no endpoint variable is set and the caller passes no
|
|
1593
|
+
* env, so the SDK keeps its own default environment.
|
|
1594
|
+
*/
|
|
1595
|
+
function buildInvocationEnv(env) {
|
|
1596
|
+
const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
|
|
1597
|
+
if (!env && !hasEndpointEnv) return void 0;
|
|
1598
|
+
const merged = withoutEmptyEndpointVars({
|
|
1599
|
+
...process.env,
|
|
1600
|
+
...env
|
|
1601
|
+
});
|
|
1602
|
+
preferOauthToken(merged);
|
|
1603
|
+
return merged;
|
|
1604
|
+
}
|
|
1558
1605
|
let nativeBinaryWarned = false;
|
|
1559
1606
|
/**
|
|
1560
1607
|
* Warn once per process when the SDK's per-platform native binary is missing:
|
|
@@ -1570,11 +1617,7 @@ function warnOnceIfNativeBinaryMissing() {
|
|
|
1570
1617
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
1571
1618
|
const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
|
|
1572
1619
|
const resolvedModel = resolveModel(model);
|
|
1573
|
-
const
|
|
1574
|
-
const mergedEnv = env || hasEndpointEnv ? withoutEmptyEndpointVars({
|
|
1575
|
-
...process.env,
|
|
1576
|
-
...env
|
|
1577
|
-
}) : void 0;
|
|
1620
|
+
const mergedEnv = buildInvocationEnv(env);
|
|
1578
1621
|
let lastAbToolUseId = null;
|
|
1579
1622
|
const claimAbToolUse = (toolUseId) => {
|
|
1580
1623
|
if (toolUseId !== lastAbToolUseId) return false;
|
|
@@ -10545,7 +10588,6 @@ async function runOneSpec(args) {
|
|
|
10545
10588
|
}
|
|
10546
10589
|
const spec = parseTestSpec(specContent);
|
|
10547
10590
|
const expanded = expandSpec(spec, { blocks: await loadAllBlocks(cwd) });
|
|
10548
|
-
const envScrubMap = buildProseEnvScrubMap(spec, expanded);
|
|
10549
10591
|
meta("spec", spec.title);
|
|
10550
10592
|
meta("steps", expanded.length);
|
|
10551
10593
|
const includes = collectIncludedBlockNames(spec);
|
|
@@ -10575,6 +10617,7 @@ async function runOneSpec(args) {
|
|
|
10575
10617
|
}
|
|
10576
10618
|
try {
|
|
10577
10619
|
const runId = buildRunId();
|
|
10620
|
+
const envScrubMap = buildProseEnvScrubMap(spec, expanded, { CCQA_RUN_ID: runId });
|
|
10578
10621
|
const runDir = opts.out ?? join(specDir, "runs", runId);
|
|
10579
10622
|
await mkdir(runDir, { recursive: true });
|
|
10580
10623
|
meta("runDir", runDir);
|
|
@@ -11810,10 +11853,11 @@ async function runVitest(scriptPath, agentBrowserSession) {
|
|
|
11810
11853
|
"--config",
|
|
11811
11854
|
bundledVitestConfigPath(),
|
|
11812
11855
|
scriptPath
|
|
11813
|
-
],
|
|
11856
|
+
], { env: {
|
|
11814
11857
|
...process.env,
|
|
11815
|
-
|
|
11816
|
-
|
|
11858
|
+
CCQA_RUN_ID: buildRunId(),
|
|
11859
|
+
...agentBrowserSession ? { AGENT_BROWSER_SESSION: agentBrowserSession } : {}
|
|
11860
|
+
} });
|
|
11817
11861
|
const currentScript = await readFile(scriptPath, "utf8");
|
|
11818
11862
|
return {
|
|
11819
11863
|
exitCode,
|
|
@@ -14005,6 +14049,10 @@ function createRunTeardown() {
|
|
|
14005
14049
|
untrackSession(name) {
|
|
14006
14050
|
sessions.delete(name);
|
|
14007
14051
|
},
|
|
14052
|
+
async closeTracked(name) {
|
|
14053
|
+
await closeSession(name);
|
|
14054
|
+
sessions.delete(name);
|
|
14055
|
+
},
|
|
14008
14056
|
onFinalize(fn) {
|
|
14009
14057
|
finalizers.push(fn);
|
|
14010
14058
|
},
|
|
@@ -14279,6 +14327,7 @@ CCQA_STEP=<step-id> agent-browser --session SESSION upload "<input[type=file] se
|
|
|
14279
14327
|
- \`@ref\` / \`@e1\` / \`e14\` — reference IDs are session-specific and change every run.
|
|
14280
14328
|
- **Bare tag selectors**: \`button\`, \`a\`, \`div\`, \`td\`, \`tr\`, \`main a\`, \`table tbody tr:nth-child(N)\`. These match every element of that tag and are non-deterministic on replay. **This includes the inner selector inside \`find first/last/nth\`** — see the \`find\` rules below.
|
|
14281
14329
|
- \`[role='button']\` or \`[type='checkbox']\` alone — matches too many elements.
|
|
14330
|
+
- **Playwright-only pseudo-classes**: \`:has-text()\`, \`:text-is()\`, \`:text-matches()\`, \`:visible\`. agent-browser's CSS engine does not implement them — they match nothing and every command using them fails. Use \`text=...\` or plain CSS instead.
|
|
14282
14331
|
- JavaScript execution (\`eval\`, \`js\`) — blocked by the hook layer.
|
|
14283
14332
|
|
|
14284
14333
|
### \`find\` subset (fallback when no ALLOWED CSS uniquely targets the element)
|
|
@@ -14660,9 +14709,9 @@ const ASSERT_TIMEOUT_MS = 1e4;
|
|
|
14660
14709
|
* has no side effect; assert types whose codegen forms aren't directly
|
|
14661
14710
|
* verifiable here fall through to the caller's `unverifiable` fallback).
|
|
14662
14711
|
*/
|
|
14663
|
-
function actionToAbArgs(action, sessionName) {
|
|
14712
|
+
function actionToAbArgs(action, sessionName, envOverrides = {}) {
|
|
14664
14713
|
const base = ["--session", sessionName];
|
|
14665
|
-
const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s);
|
|
14714
|
+
const sub = (s) => s === void 0 ? "" : resolveEnvRefs(s, envOverrides);
|
|
14666
14715
|
switch (action.action) {
|
|
14667
14716
|
case "snapshot": return null;
|
|
14668
14717
|
case "assert": return assertToAbArgs(action, sub, sessionName);
|
|
@@ -14747,8 +14796,8 @@ const NO_STEP_ID = "__no_step__";
|
|
|
14747
14796
|
* `wait <selector>`); everything else spawns the agent-browser argv. A single
|
|
14748
14797
|
* hard-timeout (SIGTERM) retry covers the daemon's occasional under-load drop.
|
|
14749
14798
|
*/
|
|
14750
|
-
function runValidationAction(action, sessionName) {
|
|
14751
|
-
const built = actionToAbArgs(action, sessionName);
|
|
14799
|
+
function runValidationAction(action, sessionName, envOverrides = {}) {
|
|
14800
|
+
const built = actionToAbArgs(action, sessionName, envOverrides);
|
|
14752
14801
|
if (built === null) return {
|
|
14753
14802
|
skipped: true,
|
|
14754
14803
|
ok: false,
|
|
@@ -14792,7 +14841,7 @@ function validateActions(actions, opts) {
|
|
|
14792
14841
|
});
|
|
14793
14842
|
continue;
|
|
14794
14843
|
}
|
|
14795
|
-
const outcome = runValidationAction(action, opts.sessionName);
|
|
14844
|
+
const outcome = runValidationAction(action, opts.sessionName, opts.envOverrides);
|
|
14796
14845
|
if (outcome.skipped) {
|
|
14797
14846
|
kept.push(action);
|
|
14798
14847
|
continue;
|
|
@@ -14869,7 +14918,7 @@ function rescueLostSteps(actions, kept, dropped, opts) {
|
|
|
14869
14918
|
for (const [stepId, drops] of lostStepDrops.entries()) {
|
|
14870
14919
|
let anyForThisStep = false;
|
|
14871
14920
|
for (const d of drops) {
|
|
14872
|
-
const outcome = runValidationAction(d.action, opts.sessionName);
|
|
14921
|
+
const outcome = runValidationAction(d.action, opts.sessionName, opts.envOverrides);
|
|
14873
14922
|
if (outcome.skipped) continue;
|
|
14874
14923
|
if (outcome.ok) {
|
|
14875
14924
|
rescuedIndices.add(d.index);
|
|
@@ -15051,12 +15100,22 @@ function formatUnstableDrop(drop) {
|
|
|
15051
15100
|
}
|
|
15052
15101
|
//#endregion
|
|
15053
15102
|
//#region src/cli/trace.ts
|
|
15103
|
+
/**
|
|
15104
|
+
* Step ids (in spec order) whose kept actions include no assertion. A step
|
|
15105
|
+
* with none produced a test that performs the step but verifies nothing
|
|
15106
|
+
* about its `expected` — surfaced as a per-step warning after validation.
|
|
15107
|
+
*/
|
|
15108
|
+
function stepsWithoutAsserts(stepIds, actions) {
|
|
15109
|
+
const withAssert = new Set(actions.filter((a) => a.action === "assert" && a.stepId !== void 0).map((a) => a.stepId));
|
|
15110
|
+
return stepIds.filter((id) => !withAssert.has(id));
|
|
15111
|
+
}
|
|
15054
15112
|
async function runTrace(featureName, specName, model, validationMode = "lenient", language, opts = {}) {
|
|
15055
15113
|
header("trace", `${featureName}/${specName}`);
|
|
15056
15114
|
await preflightAgentBrowserCommand();
|
|
15057
15115
|
const spec = parseTestSpec(await readSpecFile(featureName, specName, opts.cwd));
|
|
15058
15116
|
const expanded = expandSpec(spec, { blocks: await loadAllBlocks(opts.cwd) });
|
|
15059
|
-
const
|
|
15117
|
+
const sessionName = generateSessionName();
|
|
15118
|
+
const envScrub = buildSpecEnvScrub(spec, expanded, { CCQA_RUN_ID: sessionName });
|
|
15060
15119
|
const envScrubMap = envScrub.map;
|
|
15061
15120
|
if (envScrub.unresolved.length > 0) {
|
|
15062
15121
|
warn(`spec references env var(s) that are unset at record time: ${envScrub.unresolved.join(", ")}`);
|
|
@@ -15068,7 +15127,7 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
15068
15127
|
const includes = collectIncludedBlockNames(spec);
|
|
15069
15128
|
if (includes.length > 0) meta("blocks", includes.join(", "));
|
|
15070
15129
|
blank();
|
|
15071
|
-
|
|
15130
|
+
opts.teardown?.trackSession(sessionName);
|
|
15072
15131
|
const baseSystemPrompt = buildTraceSystemPrompt({
|
|
15073
15132
|
title: spec.title,
|
|
15074
15133
|
steps: expanded,
|
|
@@ -15151,13 +15210,18 @@ async function runTrace(featureName, specName, model, validationMode = "lenient"
|
|
|
15151
15210
|
}
|
|
15152
15211
|
});
|
|
15153
15212
|
if (isError) overallStatus = "failed";
|
|
15154
|
-
const
|
|
15155
|
-
const
|
|
15213
|
+
const dedupedActions = dedupAndReport(scrubAndReport(traceActions));
|
|
15214
|
+
const validatedActions = overallStatus === "passed" || opts.validateFailedTrace === true ? validateAndReport(dedupedActions, validationMode, { CCQA_RUN_ID: sessionName }, opts.teardown) : dedupedActions;
|
|
15215
|
+
opts.teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
|
|
15216
|
+
const recordingPath = overallStatus === "passed" ? await saveRecording(featureName, specName, validatedActions, opts.cwd) : await saveFailedRecording(featureName, specName, validatedActions, opts.cwd);
|
|
15156
15217
|
blank();
|
|
15157
15218
|
meta("saved", recordingPath);
|
|
15158
15219
|
meta("actions", validatedActions.length);
|
|
15159
15220
|
meta("status", overallStatus.toUpperCase());
|
|
15160
|
-
|
|
15221
|
+
if (overallStatus === "passed") {
|
|
15222
|
+
for (const stepId of stepsWithoutAsserts(expanded.map((s) => s.id), validatedActions)) warn(`${stepId} recorded no assertion — nothing in the generated test verifies its 'expected'`);
|
|
15223
|
+
hint(`run 'ccqa generate ${featureName}/${specName}' to generate a test script`);
|
|
15224
|
+
} else warn("trace FAILED — the recorded actions were saved beside the spec for diagnosis; the previous ir.json and generated code are left untouched");
|
|
15161
15225
|
return {
|
|
15162
15226
|
status: overallStatus,
|
|
15163
15227
|
statusLines,
|
|
@@ -15298,26 +15362,30 @@ function isAdjacentDuplicate(a, b) {
|
|
|
15298
15362
|
}
|
|
15299
15363
|
/**
|
|
15300
15364
|
* Run the post-trace replay validation and emit user-visible drop reports.
|
|
15301
|
-
* Splitting this out keeps `runTrace` readable;
|
|
15302
|
-
*
|
|
15365
|
+
* Splitting this out keeps `runTrace` readable; side effects are `log.*`,
|
|
15366
|
+
* the agent-browser invocations inside `validateActions`, and the replay
|
|
15367
|
+
* session's registration/close on `teardown`.
|
|
15303
15368
|
*
|
|
15304
15369
|
* In lenient mode (the default) failing actions are NOT removed — they're
|
|
15305
15370
|
* tagged with `replayUnstable: true` and merged back into the output stream
|
|
15306
15371
|
* in their original order so codegen can still emit them (with a `// [warn]`
|
|
15307
15372
|
* comment) and let the auto-fix loop decide what to do.
|
|
15308
15373
|
*/
|
|
15309
|
-
function validateAndReport(actions, mode) {
|
|
15374
|
+
function validateAndReport(actions, mode, envOverrides, teardown) {
|
|
15310
15375
|
if (actions.length === 0) return actions;
|
|
15311
15376
|
const sessionName = `${generateSessionName()}-validate`;
|
|
15377
|
+
teardown?.trackSession(sessionName);
|
|
15312
15378
|
blank();
|
|
15313
15379
|
info(`post-trace validation in ${mode} mode (replaying ${actions.length} recorded action(s))...`);
|
|
15314
15380
|
const { kept, unstable, dropped, rescuedSteps = [] } = validateActions(actions, {
|
|
15315
15381
|
sessionName,
|
|
15316
15382
|
mode,
|
|
15383
|
+
envOverrides,
|
|
15317
15384
|
onProgress: (i, total, action) => {
|
|
15318
15385
|
progress(i, total, validationProgressLabel(action));
|
|
15319
15386
|
}
|
|
15320
15387
|
});
|
|
15388
|
+
teardown?.closeTracked(sessionName) ?? closeSession(sessionName);
|
|
15321
15389
|
progressEnd();
|
|
15322
15390
|
if (rescuedSteps.length > 0) info(`rescued ${rescuedSteps.length} step(s) that had lost every action: ${rescuedSteps.join(", ")}`);
|
|
15323
15391
|
if (mode === "lenient") {
|
|
@@ -15540,6 +15608,10 @@ function resolveTargetOrExit(resolve) {
|
|
|
15540
15608
|
async function runGenerateLocked(featureName, specName, opts, cwd) {
|
|
15541
15609
|
const specYaml = await readSpecFile(featureName, specName, cwd);
|
|
15542
15610
|
const spec = parseTestSpec(specYaml);
|
|
15611
|
+
if (spec.mode === "live") {
|
|
15612
|
+
error(`this spec is 'mode: live' — a live spec runs without generated code. Run 'ccqa run ${featureName}/${specName}' instead`);
|
|
15613
|
+
process.exit(2);
|
|
15614
|
+
}
|
|
15543
15615
|
const config = await loadProjectConfig(cwd);
|
|
15544
15616
|
const target = resolveTargetOrExit(() => opts.targetOverride !== void 0 ? resolveTargetOverride(spec, opts.targetOverride) : resolveTarget(spec, config));
|
|
15545
15617
|
meta("target", target.id + (opts.targetOverride !== void 0 ? " (--target override)" : ""));
|
|
@@ -15712,6 +15784,10 @@ async function runRecord(specPath, opts) {
|
|
|
15712
15784
|
error(`target "${target.id}" does not use a browser recording — run 'ccqa generate ${featureName}/${specName}' instead`);
|
|
15713
15785
|
process.exit(2);
|
|
15714
15786
|
}
|
|
15787
|
+
if (spec.mode === "live") {
|
|
15788
|
+
error(`this spec is 'mode: live' — a live spec runs without a recording. Run 'ccqa run ${featureName}/${specName}' instead`);
|
|
15789
|
+
process.exit(2);
|
|
15790
|
+
}
|
|
15715
15791
|
const project = opts.hubProfile !== void 0 ? resolveProject(opts) : void 0;
|
|
15716
15792
|
if (opts.hubProfile !== void 0) await applyProfileFromOption({
|
|
15717
15793
|
profile: opts.hubProfile,
|
|
@@ -15779,6 +15855,8 @@ async function runRecord(specPath, opts) {
|
|
|
15779
15855
|
const traceResult = await runTrace(featureName, specName, opts.model, opts.traceValidation ?? "lenient", language, {
|
|
15780
15856
|
cwd: cwdForProfile,
|
|
15781
15857
|
hubContext,
|
|
15858
|
+
teardown,
|
|
15859
|
+
validateFailedTrace: opts.learnHubTracePrompt === true,
|
|
15782
15860
|
...opts.instruction ? { instruction: opts.instruction } : {},
|
|
15783
15861
|
onStep: (stepId) => {
|
|
15784
15862
|
tracingStep = stepId;
|
|
@@ -15796,7 +15874,7 @@ async function runRecord(specPath, opts) {
|
|
|
15796
15874
|
...opts.model ? { model: opts.model } : {},
|
|
15797
15875
|
...language ? { language } : {}
|
|
15798
15876
|
});
|
|
15799
|
-
if (!opts.traceOnly) generated = (await runGenerate(featureName, specName, {
|
|
15877
|
+
if (!opts.traceOnly && traceResult.status === "passed") generated = (await runGenerate(featureName, specName, {
|
|
15800
15878
|
maxRetries: parseInt(opts.autoFixMaxRetries ?? "3", 10),
|
|
15801
15879
|
fixMode: toFixMode(opts.autoFix ?? "interactive"),
|
|
15802
15880
|
force: opts.overwrite ?? false,
|
|
@@ -21079,7 +21157,7 @@ const CSS = `
|
|
|
21079
21157
|
.d-grid dt { color: var(--muted); font-size: 12px; padding-top: 1px; }
|
|
21080
21158
|
.d-grid dd { color: var(--fg-dim); }
|
|
21081
21159
|
.d-grid dd ul { list-style: none; display: flex; flex-direction: column; gap: 3px; margin: 0; padding: 0; }
|
|
21082
|
-
.d-grid dd li::before { content: "\\2022 "; color: var(--muted-2); }
|
|
21160
|
+
.d-grid dd ul li::before { content: "\\2022 "; color: var(--muted-2); }
|
|
21083
21161
|
.d-grid code { font-size: 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
|
|
21084
21162
|
/* Prose gets a measure so it stops wrapping mid-phrase in a narrow column;
|
|
21085
21163
|
paths wrap as whole chips, never inside a path. */
|
|
@@ -21088,17 +21166,20 @@ const CSS = `
|
|
|
21088
21166
|
.d-paths code { white-space: nowrap; }
|
|
21089
21167
|
.d-prose + .d-paths { margin-top: 6px; }
|
|
21090
21168
|
.manual-attest { margin-top: 14px; }
|
|
21091
|
-
.steps-box { margin-top: 14px; max-width: 900px; }
|
|
21092
|
-
.steps-box .slabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
21093
21169
|
.d-steps { margin: 4px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 10px; font-size: 13px; color: var(--fg-dim); white-space: pre-line; }
|
|
21094
21170
|
.d-steps .step-expected { display: block; margin-top: 2px; font-size: 12.5px; }
|
|
21095
|
-
.notebox { margin-top: 14px; max-width: 900px; }
|
|
21096
|
-
.notebox .nlabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
21097
21171
|
.notebox textarea { width: 100%; min-height: 54px; resize: vertical; font: inherit; font-size: 13px; color: var(--fg-dim); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 8px 10px; }
|
|
21098
|
-
.notebox .nact { margin-top: 6px; display: flex; align-items: center; gap: 8px; }
|
|
21172
|
+
.notebox .nact { margin-top: 6px; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
21099
21173
|
.notebox .nstatus { font-size: 12px; color: var(--muted); }
|
|
21100
21174
|
.notebox .nstatus.ok { color: var(--pass); }
|
|
21101
21175
|
.notebox .nstatus.err { color: var(--fail); }
|
|
21176
|
+
/* ── detail panel: one card stack — reason, contents, note ── */
|
|
21177
|
+
.c-title .c-id { display: block; font-family: var(--mono); font-size: 11px; color: var(--muted-2); margin-top: 2px; }
|
|
21178
|
+
.p-sect { margin-top: 16px; max-width: 900px; }
|
|
21179
|
+
.p-sect:first-child { margin-top: 12px; }
|
|
21180
|
+
.p-slabel { font-size: 13px; font-weight: 600; color: var(--fg); margin-bottom: 6px; }
|
|
21181
|
+
/* The one-line state note beside the finding chip: what happens next. */
|
|
21182
|
+
.p-head-note { margin-left: auto; font-size: 12.5px; color: var(--muted); }
|
|
21102
21183
|
/* The inline form an audit-dismissal or environment-attestation button
|
|
21103
21184
|
expands into, in place of the two window.prompt() calls this replaces. */
|
|
21104
21185
|
.override-form { margin-top: 10px; max-width: 480px; display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -21226,16 +21307,15 @@ const CLIENT_JS = `
|
|
|
21226
21307
|
"perspectives.mode.deterministic": "deterministic", "perspectives.mode.live": "live",
|
|
21227
21308
|
"perspectives.ov.cases": "cases", "perspectives.ov.features": "features",
|
|
21228
21309
|
"perspectives.d.preconditions": "Preconditions", "perspectives.d.startScreen": "Start screen",
|
|
21229
|
-
"perspectives.d.testCondition": "Condition", "perspectives.d.
|
|
21230
|
-
"perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
|
|
21310
|
+
"perspectives.d.testCondition": "Condition", "perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
|
|
21231
21311
|
"perspectives.d.stepExpected": "Expected:",
|
|
21232
21312
|
"perspectives.note.label": "Note",
|
|
21233
21313
|
"perspectives.note.placeholder": "Notes about this case…",
|
|
21234
21314
|
"perspectives.note.saved": "Saved",
|
|
21235
21315
|
"perspectives.note.error": "Could not save — retry",
|
|
21236
|
-
"perspectives.d.lastRed": "Most recent failure",
|
|
21237
|
-
"perspectives.d.changedSince": "Changes since the last run",
|
|
21238
21316
|
"perspectives.d.whyVerdict": "Why this verdict",
|
|
21317
|
+
"perspectives.d.contents": "What this case does",
|
|
21318
|
+
"perspectives.finding.loadFailed": "Could not load the finding's detail \u2014 open the run to read it",
|
|
21239
21319
|
"perspectives.result.openRun": "Open this run in the hub",
|
|
21240
21320
|
"perspectives.result.ci": "CI",
|
|
21241
21321
|
"perspectives.rerun.state.needsRepair": "Needs repair",
|
|
@@ -21246,26 +21326,26 @@ const CLIENT_JS = `
|
|
|
21246
21326
|
"perspectives.rerun.vsDeploy": "judged against deploy",
|
|
21247
21327
|
"perspectives.rerun.noDeployHead": "no deploy recorded for this profile",
|
|
21248
21328
|
"perspectives.rerun.changedByDeploy": "deploy {sha} changed files matched to this case",
|
|
21249
|
-
"perspectives.rerun.changesSome": "
|
|
21250
|
-
"perspectives.rerun.changesNone": "
|
|
21329
|
+
"perspectives.rerun.changesSome": "changes matched to this case since the last run (as of deploy {sha})",
|
|
21330
|
+
"perspectives.rerun.changesNone": "no changes matched to this case since the last run (as of deploy {sha})",
|
|
21251
21331
|
"perspectives.rerun.touchedCount": "{n} deployed path(s) matched this case",
|
|
21252
21332
|
"perspectives.rerun.touchedUnknown": "a deploy since the last run matched this case",
|
|
21253
21333
|
"perspectives.rerun.inProgressHint": "an audit or a run is still going, or the audit has not caught up with the deploy",
|
|
21254
|
-
"perspectives.rerun.heldHint": "
|
|
21255
|
-
"perspectives.rerun.repair.testDrift": "the generated test
|
|
21256
|
-
"perspectives.rerun.repair.specChange": "the
|
|
21257
|
-
"perspectives.rerun.repair.auditUndecided": "the audit
|
|
21258
|
-
"perspectives.rerun.repair.runFailed": "the
|
|
21259
|
-
"perspectives.rerun.why.noSelectionInRange": "a deploy in range
|
|
21260
|
-
"perspectives.rerun.why.selectionUnknown": "the
|
|
21334
|
+
"perspectives.rerun.heldHint": "an audit, auto-fix or run job is working on this case — dismissing or attesting is unavailable until it finishes",
|
|
21335
|
+
"perspectives.rerun.repair.testDrift": "the audit judged the generated test code older than the implementation — auto-fix re-records it, so nothing is needed yet",
|
|
21336
|
+
"perspectives.rerun.repair.specChange": "the audit judged that the behaviour this test assumes is gone from the code — fix or delete the test, or dismiss the finding via “{dismissButton}” if it is wrong",
|
|
21337
|
+
"perspectives.rerun.repair.auditUndecided": "the audit could not tell whether the test has drifted — review the finding, then fix the test or dismiss it via “{dismissButton}”",
|
|
21338
|
+
"perspectives.rerun.repair.runFailed": "the latest run failed — this verdict will not change until the failure's cause is addressed",
|
|
21339
|
+
"perspectives.rerun.why.noSelectionInRange": "a deploy in range carries no impact judgement, so this case runs to be safe",
|
|
21340
|
+
"perspectives.rerun.why.selectionUnknown": "the latest deploy could not be judged as affecting this case or not, so it runs to be safe",
|
|
21261
21341
|
"perspectives.rerun.why.noDeployLog": "no deploy log for this profile",
|
|
21262
21342
|
"perspectives.rerun.why.unknownDeployedSha": "the last run's deployed commit is unknown",
|
|
21263
21343
|
"perspectives.rerun.why.ambiguousDeployedSha": "a deploy landed while the last run was executing",
|
|
21264
21344
|
"perspectives.rerun.why.deployedShaNotInLog": "the last run's commit predates the retained deploy log",
|
|
21265
21345
|
"perspectives.rerun.why.gapInRange": "deploys are missing from the range",
|
|
21266
21346
|
"perspectives.rerun.why.unrecognized": "this hub reported a reason this UI does not recognise",
|
|
21267
|
-
"perspectives.rerun.fix.noSelectionInRange": "A deploy
|
|
21268
|
-
"perspectives.rerun.fix.selectionUnknown": "A deploy
|
|
21347
|
+
"perspectives.rerun.fix.noSelectionInRange": "A deploy was recorded without an impact judgement, so whether it affected this case is unknown. This happens when the judgement did not finish in time, or was disabled, at record. The next run and audit retake the result.",
|
|
21348
|
+
"perspectives.rerun.fix.selectionUnknown": "A deploy could not be judged as affecting this case or not, so the next run retakes the result.",
|
|
21269
21349
|
"perspectives.rerun.fix.noDeployLog": "Nothing has been recorded in this profile's deploy log. Wire ccqa hub deploy record into the deploy job for this environment so ccqa knows what shipped.",
|
|
21270
21350
|
"perspectives.rerun.fix.unknownDeployedSha": "The last run did not record which commit the environment was running, so it cannot be positioned in the deploy log. Runs record it once this profile has a deploy log.",
|
|
21271
21351
|
"perspectives.rerun.fix.ambiguousDeployedSha": "A deploy landed while the last run was executing, so which commit it exercised is not knowable. Re-run this case to get a clean baseline.",
|
|
@@ -21298,8 +21378,8 @@ const CLIENT_JS = `
|
|
|
21298
21378
|
"perspectives.override.noteLabel": "What was resolved, and how you checked (required)",
|
|
21299
21379
|
"perspectives.override.submit": "Record",
|
|
21300
21380
|
"perspectives.override.cancel": "Never mind",
|
|
21301
|
-
"perspectives.override.dismissHint": "
|
|
21302
|
-
"perspectives.override.envHint": "The failure stays on record
|
|
21381
|
+
"perspectives.override.dismissHint": "Withdraws the audit finding and puts this case back among the runnable. The verdict moves to “re-run needed”, and the next run's result settles it.",
|
|
21382
|
+
"perspectives.override.envHint": "The failure stays on record; only the verdict becomes “manually verified”. It lapses once a deploy reaches this case, and normal runs resume.",
|
|
21303
21383
|
"prompt.card.record": "Recording browser actions",
|
|
21304
21384
|
"prompt.card.live": "Live run (AI-driven)",
|
|
21305
21385
|
"prompt.card.playwright": "Playwright test generation",
|
|
@@ -21416,16 +21496,15 @@ const CLIENT_JS = `
|
|
|
21416
21496
|
"perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
|
|
21417
21497
|
"perspectives.ov.cases": "ケース", "perspectives.ov.features": "機能",
|
|
21418
21498
|
"perspectives.d.preconditions": "前提条件", "perspectives.d.startScreen": "開始画面",
|
|
21419
|
-
"perspectives.d.testCondition": "実行条件", "perspectives.d.
|
|
21420
|
-
"perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
|
|
21499
|
+
"perspectives.d.testCondition": "実行条件", "perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
|
|
21421
21500
|
"perspectives.d.stepExpected": "期待結果:",
|
|
21422
|
-
"perspectives.note.label": "
|
|
21501
|
+
"perspectives.note.label": "メモ",
|
|
21423
21502
|
"perspectives.note.placeholder": "このケースについてのメモ…",
|
|
21424
21503
|
"perspectives.note.saved": "保存しました",
|
|
21425
21504
|
"perspectives.note.error": "保存に失敗しました — 再試行してください",
|
|
21426
|
-
"perspectives.d.lastRed": "直近の失敗",
|
|
21427
|
-
"perspectives.d.changedSince": "前回実行以降の変更",
|
|
21428
21505
|
"perspectives.d.whyVerdict": "この判定の理由",
|
|
21506
|
+
"perspectives.d.contents": "テストの内容",
|
|
21507
|
+
"perspectives.finding.loadFailed": "詳細を読み込めませんでした。実行のページで確認してください",
|
|
21429
21508
|
"perspectives.result.openRun": "ハブでこの実行を開く",
|
|
21430
21509
|
"perspectives.result.ci": "CI",
|
|
21431
21510
|
"perspectives.rerun.state.needsRepair": "修正待ち",
|
|
@@ -21435,27 +21514,27 @@ const CLIENT_JS = `
|
|
|
21435
21514
|
"perspectives.rerun.state.verified": "検証済み",
|
|
21436
21515
|
"perspectives.rerun.vsDeploy": "判定基準: デプロイ",
|
|
21437
21516
|
"perspectives.rerun.noDeployHead": "このプロファイルにはデプロイの記録がありません",
|
|
21438
|
-
"perspectives.rerun.changedByDeploy": "デプロイ {sha}
|
|
21439
|
-
"perspectives.rerun.changesSome": "
|
|
21440
|
-
"perspectives.rerun.changesNone": "
|
|
21441
|
-
"perspectives.rerun.touchedCount": "
|
|
21442
|
-
"perspectives.rerun.touchedUnknown": "
|
|
21443
|
-
"perspectives.rerun.inProgressHint": "
|
|
21444
|
-
"perspectives.rerun.heldHint": "
|
|
21445
|
-
"perspectives.rerun.repair.testDrift": "
|
|
21446
|
-
"perspectives.rerun.repair.specChange": "
|
|
21447
|
-
"perspectives.rerun.repair.auditUndecided": "
|
|
21448
|
-
"perspectives.rerun.repair.runFailed": "
|
|
21449
|
-
"perspectives.rerun.why.noSelectionInRange": "
|
|
21450
|
-
"perspectives.rerun.why.selectionUnknown": "
|
|
21517
|
+
"perspectives.rerun.changedByDeploy": "デプロイ {sha} が、このケースに関係するファイルを変更しています",
|
|
21518
|
+
"perspectives.rerun.changesSome": "前回の実行より後に、このケースに関係する変更があります(デプロイ {sha} 時点)",
|
|
21519
|
+
"perspectives.rerun.changesNone": "前回の実行より後に、このケースに関係する変更はありません(デプロイ {sha} 時点)",
|
|
21520
|
+
"perspectives.rerun.touchedCount": "前回の実行より後のデプロイが、このケースに関係する変更を {n} 件含んでいます",
|
|
21521
|
+
"perspectives.rerun.touchedUnknown": "前回の実行より後のデプロイが、このケースに関係する変更を含んでいます",
|
|
21522
|
+
"perspectives.rerun.inProgressHint": "監査または実行のジョブが作業中か、最新デプロイに対する監査がまだ走っていません",
|
|
21523
|
+
"perspectives.rerun.heldHint": "監査・自動修正・実行のいずれかのジョブが、このケースを処理中です。終わるまで、このケースへの操作(棄却・手動確認)はできません",
|
|
21524
|
+
"perspectives.rerun.repair.testDrift": "監査が、生成済みのテストコードは実装より古いと判定しました。自動修正が録り直すので、まず対応は不要です",
|
|
21525
|
+
"perspectives.rerun.repair.specChange": "監査が、このテストの前提とする振る舞いは実装から無くなったと判定しました。テストを直すか削除するかを決めてください。指摘のほうが誤りなら、「{dismissButton}」から棄却できます",
|
|
21526
|
+
"perspectives.rerun.repair.auditUndecided": "テストが実装とズレているかどうか、監査では判断できませんでした。指摘の内容を確認して、テストを直すか、「{dismissButton}」から棄却してください",
|
|
21527
|
+
"perspectives.rerun.repair.runFailed": "直近の実行が失敗しています。失敗の原因に対処するまで、この判定は変わりません",
|
|
21528
|
+
"perspectives.rerun.why.noSelectionInRange": "影響判定なしで記録されたデプロイがあるため、念のため実行対象になっています",
|
|
21529
|
+
"perspectives.rerun.why.selectionUnknown": "直近のデプロイがこのケースに影響するかどうか判断できなかったため、念のため実行対象になっています",
|
|
21451
21530
|
"perspectives.rerun.why.noDeployLog": "このプロファイルのデプロイ記録がありません",
|
|
21452
21531
|
"perspectives.rerun.why.unknownDeployedSha": "前回実行時にデプロイされていたcommitが不明です",
|
|
21453
21532
|
"perspectives.rerun.why.ambiguousDeployedSha": "前回実行の途中でデプロイが発生しました",
|
|
21454
|
-
"perspectives.rerun.why.deployedShaNotInLog": "
|
|
21533
|
+
"perspectives.rerun.why.deployedShaNotInLog": "前回の実行が古く、どのデプロイに対して実行した結果か特定できません",
|
|
21455
21534
|
"perspectives.rerun.why.gapInRange": "対象範囲のデプロイ記録が欠けています",
|
|
21456
21535
|
"perspectives.rerun.why.unrecognized": "このUIが認識できない理由がハブから返されました",
|
|
21457
|
-
"perspectives.rerun.fix.noSelectionInRange": "
|
|
21458
|
-
"perspectives.rerun.fix.selectionUnknown": "
|
|
21536
|
+
"perspectives.rerun.fix.noSelectionInRange": "影響判定なしで記録されたデプロイがあり、このケースに影響したかどうか分かりません。デプロイの記録時に影響判定が時間内に終わらなかったか、無効化されていたときに起きます。次の実行と監査で結果を取り直します。",
|
|
21537
|
+
"perspectives.rerun.fix.selectionUnknown": "デプロイがこのケースに影響するかどうか判断できなかったため、次の実行で結果を取り直します。",
|
|
21459
21538
|
"perspectives.rerun.fix.noDeployLog": "このプロファイルのデプロイログに記録がありません。何がデプロイされたかをccqaに伝えるため、この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
21460
21539
|
"perspectives.rerun.fix.unknownDeployedSha": "前回実行は環境で動いていたcommitを記録していないため、デプロイログ上の位置を決められません。このプロファイルにデプロイログができれば、以降の実行では記録されます。",
|
|
21461
21540
|
"perspectives.rerun.fix.ambiguousDeployedSha": "前回実行の途中でデプロイが発生したため、どのcommitを検証したのか確定できません。基準を取り直すには再実行してください。",
|
|
@@ -21471,25 +21550,25 @@ const CLIENT_JS = `
|
|
|
21471
21550
|
"perspectives.manual.envButton": "環境要因が解消した場合はこちら",
|
|
21472
21551
|
"perspectives.manual.confirmRevoke": "このケースの手動確認を取り消しますか?",
|
|
21473
21552
|
"perspectives.manual.error": "保存に失敗しました — 再試行してください",
|
|
21474
|
-
"perspectives.manual.verifiedBy": "{by}
|
|
21475
|
-
"perspectives.manual.lapsed.deployReached": "
|
|
21476
|
-
"perspectives.manual.lapsed.deployReachedNamed": "
|
|
21477
|
-
"perspectives.manual.lapsed.cannotPlace": "
|
|
21478
|
-
"perspectives.manual.lapsed.specEdited": "
|
|
21479
|
-
"perspectives.manual.lapsed.newerRed": "
|
|
21480
|
-
"perspectives.manual.lapsed.unrecognized": "このUI
|
|
21553
|
+
"perspectives.manual.verifiedBy": "{by} さんが手動で動作確認しました({at})",
|
|
21554
|
+
"perspectives.manual.lapsed.deployReached": "デプロイがこのケースに届いたため、手動確認は失効しました",
|
|
21555
|
+
"perspectives.manual.lapsed.deployReachedNamed": "デプロイ {sha}({at})がこのケースに届いたため、手動確認は失効しました",
|
|
21556
|
+
"perspectives.manual.lapsed.cannotPlace": "基準のデプロイをログで特定できなくなったため、手動確認は失効しました",
|
|
21557
|
+
"perspectives.manual.lapsed.specEdited": "spec が編集されたため、手動確認は失効しました",
|
|
21558
|
+
"perspectives.manual.lapsed.newerRed": "手動確認より後の実行が失敗したため、手動確認は失効しました",
|
|
21559
|
+
"perspectives.manual.lapsed.unrecognized": "この UI が認識できない理由により、手動確認は失効しました",
|
|
21481
21560
|
"perspectives.dismiss.offerButton": "テスト仕様に問題がなかった場合はこちら",
|
|
21482
21561
|
"perspectives.dismiss.revokeButton": "棄却を取り消す",
|
|
21483
21562
|
"perspectives.dismiss.confirmRevoke": "このケースの棄却を取り消しますか?",
|
|
21484
|
-
"perspectives.dismiss.activeNote": "
|
|
21485
|
-
"perspectives.dismiss.priorNote": "
|
|
21563
|
+
"perspectives.dismiss.activeNote": "{by} さんが監査指摘「{headline}」を誤検知として棄却しました({at}・理由: {note})。次の実行結果が正否を決めます。",
|
|
21564
|
+
"perspectives.dismiss.priorNote": "この指摘は、以前 {by} さんが棄却しています({at}・理由: {note})",
|
|
21486
21565
|
"perspectives.override.byLabel": "確認した人",
|
|
21487
21566
|
"perspectives.override.reasonLabel": "理由(必須)",
|
|
21488
21567
|
"perspectives.override.noteLabel": "解消と確認の内容(必須)",
|
|
21489
21568
|
"perspectives.override.submit": "記録する",
|
|
21490
21569
|
"perspectives.override.cancel": "やめる",
|
|
21491
|
-
"perspectives.override.dismissHint": "
|
|
21492
|
-
"perspectives.override.envHint": "
|
|
21570
|
+
"perspectives.override.dismissHint": "監査の指摘を取り下げて、このケースを実行対象に戻します。判定は「要再実行」になり、次の実行結果が正否を決めます。",
|
|
21571
|
+
"perspectives.override.envHint": "失敗の記録は残したまま、判定だけが「手動確認済み」になります。次のデプロイがこのケースに届くと失効し、通常の実行に戻ります。",
|
|
21493
21572
|
"prompt.card.record": "ブラウザ操作の記録",
|
|
21494
21573
|
"prompt.card.live": "ライブ実行(AI操作)",
|
|
21495
21574
|
"prompt.card.playwright": "Playwrightテスト生成",
|
|
@@ -21786,7 +21865,6 @@ const CLIENT_JS = `
|
|
|
21786
21865
|
return span;
|
|
21787
21866
|
}
|
|
21788
21867
|
|
|
21789
|
-
|
|
21790
21868
|
// Shared by the runs-list row and the run-detail header — the one place
|
|
21791
21869
|
// both decide whether a run's own status badge speaks drift's vocabulary.
|
|
21792
21870
|
function runStatusBadge(run) {
|
|
@@ -22396,15 +22474,13 @@ const CLIENT_JS = `
|
|
|
22396
22474
|
|
|
22397
22475
|
// ── run detail: spec cards ──────────────────────────────────────────
|
|
22398
22476
|
|
|
22399
|
-
// The diagnosis card
|
|
22400
|
-
//
|
|
22401
|
-
//
|
|
22402
|
-
//
|
|
22403
|
-
//
|
|
22404
|
-
//
|
|
22405
|
-
function
|
|
22406
|
-
var wrap = el("div", "analysis-box");
|
|
22407
|
-
var a = r.analysis;
|
|
22477
|
+
// The diagnosis card's two halves, shared by the run view (analysisSection)
|
|
22478
|
+
// and the perspectives reason card so the two renderings cannot drift:
|
|
22479
|
+
// verdict head (label chip + confidence), then the cause→fix pair as
|
|
22480
|
+
// labelled rows — headline and recommendation are one causal unit, so they
|
|
22481
|
+
// read as one. subDiagnosis is deliberately NOT shown: it is a machine
|
|
22482
|
+
// vocabulary for accuracy stratification and learning, not for humans.
|
|
22483
|
+
function diagnosisHead(a) {
|
|
22408
22484
|
var head = el("div", "analysis-head");
|
|
22409
22485
|
head.appendChild(labelChip(a.label));
|
|
22410
22486
|
// Which repair a SPEC_CHANGE needs — delete the spec, or rewrite it. The
|
|
@@ -22414,7 +22490,10 @@ const CLIENT_JS = `
|
|
|
22414
22490
|
head.appendChild(el("span", "chip spec-change-chip", t("diag.specChangeKind." + a.specChangeKind)));
|
|
22415
22491
|
}
|
|
22416
22492
|
head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
|
|
22417
|
-
|
|
22493
|
+
return head;
|
|
22494
|
+
}
|
|
22495
|
+
|
|
22496
|
+
function diagnosisKv(a) {
|
|
22418
22497
|
var kv = el("div", "analysis-kv");
|
|
22419
22498
|
// Set only when the verdict blames the test case (TEST_DRIFT/SPEC_CHANGE),
|
|
22420
22499
|
// on both kinds of row — it names the half that has to be repaired.
|
|
@@ -22430,7 +22509,14 @@ const CLIENT_JS = `
|
|
|
22430
22509
|
kv.appendChild(el("div", "k", t("diag.fix")));
|
|
22431
22510
|
kv.appendChild(el("div", "v", a.recommendation));
|
|
22432
22511
|
}
|
|
22433
|
-
|
|
22512
|
+
return kv.childNodes.length > 0 ? kv : null;
|
|
22513
|
+
}
|
|
22514
|
+
|
|
22515
|
+
function analysisSection(runId, r) {
|
|
22516
|
+
var wrap = el("div", "analysis-box");
|
|
22517
|
+
wrap.appendChild(diagnosisHead(r.analysis));
|
|
22518
|
+
var kv = diagnosisKv(r.analysis);
|
|
22519
|
+
if (kv) wrap.appendChild(kv);
|
|
22434
22520
|
return wrap;
|
|
22435
22521
|
}
|
|
22436
22522
|
|
|
@@ -23543,9 +23629,10 @@ const CLIENT_JS = `
|
|
|
23543
23629
|
// this hub answers at all. Chip visibility follows it rather than the report,
|
|
23544
23630
|
// so switching profile doesn't drop the filter while the next one loads.
|
|
23545
23631
|
// "drift" is the DriftLedgerResponse, or null when unanswered (older hub, or
|
|
23546
|
-
// a failed fetch) — not profile-scoped
|
|
23547
|
-
//
|
|
23548
|
-
//
|
|
23632
|
+
// a failed fetch) — not profile-scoped. It backs the audit column's
|
|
23633
|
+
// "audited at" line and the reason card's finding (runId/label/headline),
|
|
23634
|
+
// so reloadRerun refetches it alongside the rerun report to keep the two
|
|
23635
|
+
// reports of one card equally fresh.
|
|
23549
23636
|
var perspState = {
|
|
23550
23637
|
doc: null, q: "", f: "all",
|
|
23551
23638
|
rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
|
|
@@ -23613,10 +23700,10 @@ const CLIENT_JS = `
|
|
|
23613
23700
|
|
|
23614
23701
|
// ── perspectives: drift ledger ────────────────────────────────────────
|
|
23615
23702
|
// Not profile-scoped (see perspState.drift above), so unlike rerunPath this
|
|
23616
|
-
// takes no ?profile=.
|
|
23617
|
-
//
|
|
23618
|
-
//
|
|
23619
|
-
// column's evidence line
|
|
23703
|
+
// takes no ?profile=. The audit AXIS is answered by the /rerun report
|
|
23704
|
+
// (ADR-0014); this ledger supplies what that report does not carry — the
|
|
23705
|
+
// finding's own coordinate and words (runId/at/label/headline), shown in
|
|
23706
|
+
// the audit column's evidence line and the reason card.
|
|
23620
23707
|
|
|
23621
23708
|
function driftPath() {
|
|
23622
23709
|
return "/api/v1/projects/" + encodeURIComponent(state.project) + "/drift";
|
|
@@ -23669,7 +23756,10 @@ const CLIENT_JS = `
|
|
|
23669
23756
|
// when it has no entry).
|
|
23670
23757
|
function rerunReasonText(prefix, reason) {
|
|
23671
23758
|
var text = t(prefix + reason);
|
|
23672
|
-
|
|
23759
|
+
if (text === prefix + reason) text = t(prefix + "unrecognized");
|
|
23760
|
+
// Wordings that point at the dismiss control name it by its own label, so
|
|
23761
|
+
// renaming the button cannot silently strand four strings.
|
|
23762
|
+
return text.replace("{dismissButton}", t("perspectives.dismiss.offerButton"));
|
|
23673
23763
|
}
|
|
23674
23764
|
|
|
23675
23765
|
// Who attested and when, plus their note if they left one — the whole
|
|
@@ -23760,7 +23850,6 @@ const CLIENT_JS = `
|
|
|
23760
23850
|
return lapse ? why + " · " + lapse : why;
|
|
23761
23851
|
}
|
|
23762
23852
|
|
|
23763
|
-
|
|
23764
23853
|
// --- pure: rerun composition ---------------------------------------------
|
|
23765
23854
|
// Self-contained on purpose: no DOM, no closures. rerun-view.test.ts lifts
|
|
23766
23855
|
// this region out of the rendered page and runs it, because the summary bar
|
|
@@ -24202,8 +24291,8 @@ const CLIENT_JS = `
|
|
|
24202
24291
|
|
|
24203
24292
|
// --- pure: rerun detail labels -------------------------------------------
|
|
24204
24293
|
// Self-contained on purpose (no DOM, no closures) so rerun-view.test.ts can
|
|
24205
|
-
// lift this region out of the rendered page and run it:
|
|
24206
|
-
//
|
|
24294
|
+
// lift this region out of the rendered page and run it: whether the deploy
|
|
24295
|
+
// log answered for this case, and which deploy the reason line names.
|
|
24207
24296
|
|
|
24208
24297
|
// The deploy log answered for this case: the row can show what it holds.
|
|
24209
24298
|
// A case the log could not place has no evidence to show even though its
|
|
@@ -24213,20 +24302,6 @@ const CLIENT_JS = `
|
|
|
24213
24302
|
return rr.verdict === "rerunNeeded" && !rr.executionAssumedReached;
|
|
24214
24303
|
}
|
|
24215
24304
|
|
|
24216
|
-
// Evidence is labelled by the timeframe it covers; everything else names why
|
|
24217
|
-
// the verdict landed — a different kind of content, and forcing one label
|
|
24218
|
-
// over both would make one of the two read as a lie.
|
|
24219
|
-
function rerunEvidenceLabelKey(rr) {
|
|
24220
|
-
return rerunHasEvidence(rr) ? "perspectives.d.changedSince" : "perspectives.d.whyVerdict";
|
|
24221
|
-
}
|
|
24222
|
-
|
|
24223
|
-
// The failure row points at a run. With no failure there is nothing to point
|
|
24224
|
-
// at, so the row is omitted rather than filled with "never failed" — the row
|
|
24225
|
-
// above already carries the last result.
|
|
24226
|
-
function rerunHasFailure(rr) {
|
|
24227
|
-
return !!(rr && rr.lastRed);
|
|
24228
|
-
}
|
|
24229
|
-
|
|
24230
24305
|
// Which deploy the evidence line names, and how. A "needed" verdict carries
|
|
24231
24306
|
// the deploy that caused it (touchedByDeploy) when the hub could confirm one,
|
|
24232
24307
|
// and that is the deploy a reader wants — so it is named, with when it
|
|
@@ -24293,13 +24368,10 @@ const CLIENT_JS = `
|
|
|
24293
24368
|
return null;
|
|
24294
24369
|
}
|
|
24295
24370
|
|
|
24296
|
-
// The
|
|
24297
|
-
//
|
|
24298
|
-
//
|
|
24299
|
-
//
|
|
24300
|
-
// A dismissal (active or superseded by a later finding) is appended below
|
|
24301
|
-
// whichever of those this case has, rather than replacing it — see
|
|
24302
|
-
// rerunDismissalLine.
|
|
24371
|
+
// The reason card's body when no run-recorded finding is shown: what the
|
|
24372
|
+
// deploy log holds since this case last ran (rerunChangeLine), or why the
|
|
24373
|
+
// verdict landed. The dismissal and lapse notes are the card's own to
|
|
24374
|
+
// append (perspReasonCard), not this value's.
|
|
24303
24375
|
function rerunEvidenceValue(rr) {
|
|
24304
24376
|
var wrap = el("div");
|
|
24305
24377
|
if (!rerunHasEvidence(rr)) {
|
|
@@ -24319,29 +24391,12 @@ const CLIENT_JS = `
|
|
|
24319
24391
|
wrap.appendChild(pathCodes(rr.touchedBy));
|
|
24320
24392
|
}
|
|
24321
24393
|
}
|
|
24322
|
-
var dismissLine = rerunDismissalLine(rr);
|
|
24323
|
-
if (dismissLine) wrap.appendChild(el("div", "d-prose" + (dismissLine.muted ? " muted" : ""), dismissLine.text));
|
|
24324
|
-
return wrap;
|
|
24325
|
-
}
|
|
24326
|
-
|
|
24327
|
-
// The failure: which run it was, then what the analysis concluded. The
|
|
24328
|
-
// headline is model output, already localized server-side, so it is shown as
|
|
24329
|
-
// written. A run made without failure analysis carries neither field, and the
|
|
24330
|
-
// row is then the coordinate alone — what it has always been.
|
|
24331
|
-
function rerunFailureValue(entry) {
|
|
24332
|
-
var wrap = el("div");
|
|
24333
|
-
wrap.appendChild(ledgerLine(entry));
|
|
24334
|
-
if (entry.label) {
|
|
24335
|
-
var line = el("div", "d-prose", labelText(entry.label));
|
|
24336
|
-
if (entry.headline) line.appendChild(document.createTextNode(" · " + entry.headline));
|
|
24337
|
-
wrap.appendChild(line);
|
|
24338
|
-
}
|
|
24339
24394
|
return wrap;
|
|
24340
24395
|
}
|
|
24341
24396
|
|
|
24342
24397
|
// Lets a person's own check stand in for the machine's verdict. reloadRerun()
|
|
24343
|
-
// is the same
|
|
24344
|
-
//
|
|
24398
|
+
// is the same refresh a profile switch uses — it re-renders the whole
|
|
24399
|
+
// table, so an open detail panel closes along with it.
|
|
24345
24400
|
function submitAttestation(method, body) {
|
|
24346
24401
|
apiFetch(attestationsPath(), {
|
|
24347
24402
|
method: method,
|
|
@@ -24458,7 +24513,10 @@ const CLIENT_JS = `
|
|
|
24458
24513
|
// dismissal that is currently the reason the axis reads clean. At most one
|
|
24459
24514
|
// of the two ever shows — a finding the axis
|
|
24460
24515
|
// itself has cleared, dismissed or not, offers nothing here.
|
|
24516
|
+
// While a job holds the spec, no control shows at all: heldHint promises
|
|
24517
|
+
// the reader that overrides wait for the job, so the buttons must too.
|
|
24461
24518
|
function auditOverrideBox(feature, spec, rr) {
|
|
24519
|
+
if (rr.heldBy) return null;
|
|
24462
24520
|
if (auditDismissalActive(rr)) {
|
|
24463
24521
|
var box = el("div", "manual-attest");
|
|
24464
24522
|
box.appendChild(auditDismissalRevokeButton(feature, spec));
|
|
@@ -24473,6 +24531,7 @@ const CLIENT_JS = `
|
|
|
24473
24531
|
// no open finding of its own, which is auditOverrideBox's problem to answer,
|
|
24474
24532
|
// not this one's.
|
|
24475
24533
|
function executionOverrideBox(feature, spec, rr) {
|
|
24534
|
+
if (rr.heldBy) return null;
|
|
24476
24535
|
if (rr.manual) {
|
|
24477
24536
|
var box = el("div", "manual-attest");
|
|
24478
24537
|
if (rr.verdict !== "manuallyVerified") box.appendChild(el("div", "d-prose", manualAttestationText(rr.manual)));
|
|
@@ -24487,16 +24546,133 @@ const CLIENT_JS = `
|
|
|
24487
24546
|
return null;
|
|
24488
24547
|
}
|
|
24489
24548
|
|
|
24490
|
-
//
|
|
24491
|
-
//
|
|
24492
|
-
//
|
|
24493
|
-
//
|
|
24494
|
-
//
|
|
24495
|
-
//
|
|
24496
|
-
//
|
|
24497
|
-
|
|
24549
|
+
// ── the reason card ──────────────────────────────────────────────────────
|
|
24550
|
+
// One card that answers "why is the verdict what it is". When an axis
|
|
24551
|
+
// stands on a run-recorded finding (an open audit finding, or the failure
|
|
24552
|
+
// the execution axis reports), the card carries that finding in full,
|
|
24553
|
+
// fetched from the run's own report — where cause, fix and evidence
|
|
24554
|
+
// already live. Everything a person may do about the state (dismiss,
|
|
24555
|
+
// attest, revoke) sits in the same card, beside the reason it answers.
|
|
24556
|
+
|
|
24557
|
+
var runReportCache = {};
|
|
24558
|
+
function fetchRunReport(runId) {
|
|
24559
|
+
if (!runReportCache[runId]) {
|
|
24560
|
+
// Same endpoint (and so the same HTTP cache entry) the run view reads.
|
|
24561
|
+
// A failure is not cached: a transient 502 costs one refetch on the
|
|
24562
|
+
// next expand instead of pinning the fallback for the session.
|
|
24563
|
+
runReportCache[runId] = apiFetch("/api/v1/runs/" + encodeURIComponent(runId) + "/report")
|
|
24564
|
+
.catch(function () { delete runReportCache[runId]; return null; });
|
|
24565
|
+
}
|
|
24566
|
+
return runReportCache[runId];
|
|
24567
|
+
}
|
|
24568
|
+
|
|
24569
|
+
function reportRowFor(report, key) {
|
|
24570
|
+
var rows = (report && report.results) || [];
|
|
24571
|
+
for (var i = 0; i < rows.length; i++) {
|
|
24572
|
+
if (rows[i].feature + "/" + rows[i].spec === key) return rows[i];
|
|
24573
|
+
}
|
|
24574
|
+
return null;
|
|
24575
|
+
}
|
|
24576
|
+
|
|
24577
|
+
// Which run-recorded finding the card shows, if any: an open audit finding
|
|
24578
|
+
// wins (it is why nothing runs), else the failure the execution axis stands
|
|
24579
|
+
// on. The ledger's own label/headline are the instant fallback while the
|
|
24580
|
+
// report loads — and the whole content if it never arrives.
|
|
24581
|
+
function reasonFindingSource(rr, driftEntry) {
|
|
24582
|
+
if (auditOpen(rr) && driftEntry && driftEntry.runId && driftEntry.label) return driftEntry;
|
|
24583
|
+
if (rr.execution === "failed" && rr.lastRed && rr.lastRed.runId && rr.lastRed.label) return rr.lastRed;
|
|
24584
|
+
return null;
|
|
24585
|
+
}
|
|
24586
|
+
|
|
24587
|
+
function perspReasonCard(feature, spec, rr, driftEntry) {
|
|
24588
|
+
var card = el("div", "analysis-box");
|
|
24589
|
+
var source = reasonFindingSource(rr, driftEntry);
|
|
24590
|
+
|
|
24591
|
+
if (source) {
|
|
24592
|
+
// The ledger's label/headline render at once; the run's own report
|
|
24593
|
+
// replaces them with the full diagnosis (the same head/kv the run view
|
|
24594
|
+
// builds) when — and if — it arrives.
|
|
24595
|
+
var note = el("span", "p-head-note", rerunWhyVerdict(rr));
|
|
24596
|
+
var slot = el("div");
|
|
24597
|
+
var head = el("div", "analysis-head");
|
|
24598
|
+
head.appendChild(labelChip(source.label));
|
|
24599
|
+
head.appendChild(note);
|
|
24600
|
+
slot.appendChild(head);
|
|
24601
|
+
if (source.headline) {
|
|
24602
|
+
var kv = diagnosisKv({ headline: source.headline });
|
|
24603
|
+
if (kv) slot.appendChild(kv);
|
|
24604
|
+
}
|
|
24605
|
+
card.appendChild(slot);
|
|
24606
|
+
|
|
24607
|
+
// A graded finding is the human's word, not the model's: the ledger
|
|
24608
|
+
// carries the corrected label/headline, while the run's report still
|
|
24609
|
+
// holds the original prediction. Upgrading would show the guess the
|
|
24610
|
+
// person explicitly overwrote, so the card keeps the ledger's version.
|
|
24611
|
+
if (source.graded) return finishReasonCard(card, feature, spec, rr);
|
|
24612
|
+
|
|
24613
|
+
fetchRunReport(source.runId).then(function (report) {
|
|
24614
|
+
var reportRow = reportRowFor(report, perspSpecKey(feature, spec));
|
|
24615
|
+
var a = reportRow && reportRow.analysis;
|
|
24616
|
+
if (!a) {
|
|
24617
|
+
if (!source.headline) slot.appendChild(el("div", "d-prose muted", t("perspectives.finding.loadFailed")));
|
|
24618
|
+
return;
|
|
24619
|
+
}
|
|
24620
|
+
// The ledger's one-liner stands in when the report row lost its own.
|
|
24621
|
+
if (!a.headline) a.headline = source.headline || "";
|
|
24622
|
+
clear(slot);
|
|
24623
|
+
var fullHead = diagnosisHead(a);
|
|
24624
|
+
fullHead.appendChild(note);
|
|
24625
|
+
slot.appendChild(fullHead);
|
|
24626
|
+
var fullKv = diagnosisKv(a);
|
|
24627
|
+
if (fullKv) slot.appendChild(fullKv);
|
|
24628
|
+
var evi = analysisEvidenceSection(reportRow);
|
|
24629
|
+
if (evi.count) slot.appendChild(detailsBlock(t("acc.evidence"), evi.count, evi.node));
|
|
24630
|
+
});
|
|
24631
|
+
} else {
|
|
24632
|
+
// No finding to show: the reason is the deploy-log answer, or the
|
|
24633
|
+
// verdict's own wording.
|
|
24634
|
+
card.appendChild(rerunEvidenceValue(rr));
|
|
24635
|
+
}
|
|
24636
|
+
|
|
24637
|
+
return finishReasonCard(card, feature, spec, rr);
|
|
24638
|
+
}
|
|
24639
|
+
|
|
24640
|
+
// The card's shared tail: the dismissal/lapse notes and the person's
|
|
24641
|
+
// controls, appended after whichever body the card ended up with.
|
|
24642
|
+
function finishReasonCard(card, feature, spec, rr) {
|
|
24643
|
+
var dline = rerunDismissalLine(rr);
|
|
24644
|
+
if (dline) card.appendChild(el("div", "d-prose" + (dline.muted ? " muted" : ""), dline.text));
|
|
24645
|
+
var lapse = rerunManualLapseText(rr);
|
|
24646
|
+
if (lapse) card.appendChild(el("div", "d-prose muted", lapse));
|
|
24647
|
+
var auditBox = auditOverrideBox(feature, spec, rr);
|
|
24648
|
+
if (auditBox) card.appendChild(auditBox);
|
|
24649
|
+
var execBox = executionOverrideBox(feature, spec, rr);
|
|
24650
|
+
if (execBox) card.appendChild(execBox);
|
|
24651
|
+
return card;
|
|
24652
|
+
}
|
|
24653
|
+
|
|
24654
|
+
// Detail row: the case's current state first, then why the verdict is what
|
|
24655
|
+
// it is, then what the case does, then the note — a stack of cards in the
|
|
24656
|
+
// order a reader asks the questions. Built with createElement/textContent
|
|
24657
|
+
// throughout — every field here is API-derived, so none of it may go
|
|
24658
|
+
// through innerHTML.
|
|
24498
24659
|
function perspDetailContent(feature, spec) {
|
|
24499
24660
|
var frag = document.createDocumentFragment();
|
|
24661
|
+
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
24662
|
+
var driftEntry = ledgerEntryFor(perspState.drift, feature, spec);
|
|
24663
|
+
|
|
24664
|
+
// The two axis states stay in the table row only — the panel answers why,
|
|
24665
|
+
// not what, so it opens straight on the reason.
|
|
24666
|
+
if (rr) {
|
|
24667
|
+
var reason = el("div", "p-sect");
|
|
24668
|
+
reason.appendChild(el("div", "p-slabel", t("perspectives.d.whyVerdict")));
|
|
24669
|
+
reason.appendChild(perspReasonCard(feature, spec, rr, driftEntry));
|
|
24670
|
+
frag.appendChild(reason);
|
|
24671
|
+
}
|
|
24672
|
+
|
|
24673
|
+
var contents = el("div", "p-sect");
|
|
24674
|
+
contents.appendChild(el("div", "p-slabel", t("perspectives.d.contents")));
|
|
24675
|
+
var ccard = el("div", "analysis-box");
|
|
24500
24676
|
var dl = el("dl", "d-grid");
|
|
24501
24677
|
function row(labelKey, valueNode) {
|
|
24502
24678
|
dl.appendChild(el("dt", null, t(labelKey)));
|
|
@@ -24513,20 +24689,7 @@ const CLIENT_JS = `
|
|
|
24513
24689
|
}
|
|
24514
24690
|
if (spec.startScreen) row("perspectives.d.startScreen", spec.startScreen);
|
|
24515
24691
|
if (spec.testCondition) row("perspectives.d.testCondition", spec.testCondition);
|
|
24516
|
-
// The spec id stays: it is what a user types to re-run this case, and the
|
|
24517
|
-
// table shows the title, never the id.
|
|
24518
|
-
row("perspectives.d.spec", el("code", null, spec.specName));
|
|
24519
|
-
|
|
24520
|
-
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
24521
|
-
if (rr) {
|
|
24522
|
-
row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
|
|
24523
|
-
if (rerunHasFailure(rr)) row("perspectives.d.lastRed", rerunFailureValue(rr.lastRed));
|
|
24524
|
-
}
|
|
24525
|
-
frag.appendChild(dl);
|
|
24526
|
-
|
|
24527
24692
|
if (spec.steps && spec.steps.length) {
|
|
24528
|
-
var stepsBox = el("div", "steps-box");
|
|
24529
|
-
stepsBox.appendChild(el("div", "slabel", t("perspectives.d.steps")));
|
|
24530
24693
|
var stepsList = el("ol", "d-steps");
|
|
24531
24694
|
spec.steps.forEach(function (step) {
|
|
24532
24695
|
var li = el("li");
|
|
@@ -24540,22 +24703,14 @@ const CLIENT_JS = `
|
|
|
24540
24703
|
}
|
|
24541
24704
|
stepsList.appendChild(li);
|
|
24542
24705
|
});
|
|
24543
|
-
|
|
24544
|
-
frag.appendChild(stepsBox);
|
|
24706
|
+
row("perspectives.d.steps", stepsList);
|
|
24545
24707
|
}
|
|
24708
|
+
ccard.appendChild(dl);
|
|
24709
|
+
contents.appendChild(ccard);
|
|
24710
|
+
frag.appendChild(contents);
|
|
24546
24711
|
|
|
24547
|
-
|
|
24548
|
-
|
|
24549
|
-
// standing one (auditOverrideBox / executionOverrideBox).
|
|
24550
|
-
if (rr) {
|
|
24551
|
-
var auditBox = auditOverrideBox(feature, spec, rr);
|
|
24552
|
-
if (auditBox) frag.appendChild(auditBox);
|
|
24553
|
-
var execBox = executionOverrideBox(feature, spec, rr);
|
|
24554
|
-
if (execBox) frag.appendChild(execBox);
|
|
24555
|
-
}
|
|
24556
|
-
|
|
24557
|
-
var notebox = el("div", "notebox");
|
|
24558
|
-
notebox.appendChild(el("div", "nlabel", t("perspectives.note.label")));
|
|
24712
|
+
var notebox = el("div", "notebox p-sect");
|
|
24713
|
+
notebox.appendChild(el("div", "p-slabel", t("perspectives.note.label")));
|
|
24559
24714
|
var ta = el("textarea");
|
|
24560
24715
|
ta.placeholder = t("perspectives.note.placeholder");
|
|
24561
24716
|
ta.value = spec.note || "";
|
|
@@ -24564,8 +24719,8 @@ const CLIENT_JS = `
|
|
|
24564
24719
|
var saveBtn = el("button", "btn primary", t("common.save"));
|
|
24565
24720
|
saveBtn.type = "button";
|
|
24566
24721
|
var statusEl = el("span", "nstatus");
|
|
24567
|
-
nact.appendChild(saveBtn);
|
|
24568
24722
|
nact.appendChild(statusEl);
|
|
24723
|
+
nact.appendChild(saveBtn);
|
|
24569
24724
|
notebox.appendChild(nact);
|
|
24570
24725
|
frag.appendChild(notebox);
|
|
24571
24726
|
|
|
@@ -24621,6 +24776,7 @@ const CLIENT_JS = `
|
|
|
24621
24776
|
|
|
24622
24777
|
var titleTd = el("td", "c-title");
|
|
24623
24778
|
titleTd.appendChild(document.createTextNode(spec.title));
|
|
24779
|
+
titleTd.appendChild(el("span", "c-id", perspSpecKey(feature, spec)));
|
|
24624
24780
|
if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
|
|
24625
24781
|
row.appendChild(titleTd);
|
|
24626
24782
|
|
|
@@ -24753,8 +24909,8 @@ const CLIENT_JS = `
|
|
|
24753
24909
|
loadRerun().catch(function (err) {
|
|
24754
24910
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.loadFailed") + ": " + err.message, "warn");
|
|
24755
24911
|
}),
|
|
24756
|
-
//
|
|
24757
|
-
//
|
|
24912
|
+
// A failed or unsupported fetch just omits the "audited at" line
|
|
24913
|
+
// and the reason card's finding detail, no banner.
|
|
24758
24914
|
loadDrift().catch(function () {}),
|
|
24759
24915
|
]);
|
|
24760
24916
|
})
|
|
@@ -24893,29 +25049,32 @@ const CLIENT_JS = `
|
|
|
24893
25049
|
});
|
|
24894
25050
|
}
|
|
24895
25051
|
|
|
24896
|
-
// Loaded
|
|
24897
|
-
//
|
|
24898
|
-
//
|
|
24899
|
-
//
|
|
24900
|
-
// axis in the /rerun report — so there is nothing here worth a banner on
|
|
24901
|
-
// an older or unreachable hub.
|
|
25052
|
+
// Loaded on project open and again on every reloadRerun: the reason card
|
|
25053
|
+
// joins rr.audit against this ledger's entry (runId/headline), so the two
|
|
25054
|
+
// must not drift apart after a dismissal or attestation. An older or
|
|
25055
|
+
// unreachable hub degrades to the axis alone — not worth a banner.
|
|
24902
25056
|
function loadDrift() {
|
|
24903
25057
|
return fetch(driftPath(), { headers: { Authorization: "Bearer " + state.token } })
|
|
24904
25058
|
.then(function (res) { return res.ok ? res.json() : null; }, function () { return null; })
|
|
24905
25059
|
.then(function (report) {
|
|
24906
|
-
|
|
25060
|
+
// A transient failure keeps the copy already loaded — blanking it
|
|
25061
|
+
// would drop the audit coordinates and the reason card's finding
|
|
25062
|
+
// for the rest of the session.
|
|
25063
|
+
if (report) perspState.drift = report;
|
|
24907
25064
|
renderPerspectives();
|
|
24908
25065
|
});
|
|
24909
25066
|
}
|
|
24910
25067
|
|
|
24911
|
-
//
|
|
24912
|
-
//
|
|
24913
|
-
//
|
|
25068
|
+
// Re-asks the rerun question and refreshes the drift ledger beside it (the
|
|
25069
|
+
// reason card reads both; see perspState.drift). The perspectives document
|
|
25070
|
+
// itself is project-scoped and does not change, and neither does the run
|
|
25071
|
+
// index loadRerun used to (wastefully) re-fetch.
|
|
24914
25072
|
function reloadRerun() {
|
|
24915
25073
|
perspState.rerun = null;
|
|
24916
25074
|
setPerspNote("persp-rerun-note", "");
|
|
24917
25075
|
setPerspDeployHead(null);
|
|
24918
25076
|
renderPerspectives();
|
|
25077
|
+
loadDrift().catch(function () {});
|
|
24919
25078
|
return loadRerun();
|
|
24920
25079
|
}
|
|
24921
25080
|
|
package/dist/package.json
CHANGED