ccqa 1.19.0 → 1.20.1
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 +3 -2
- package/dist/bin/ccqa.mjs +524 -398
- package/dist/hub-client/index.d.mts +14 -8
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -6779,18 +6779,16 @@ const AuditStateSchema = z.enum([
|
|
|
6779
6779
|
"due",
|
|
6780
6780
|
"clean",
|
|
6781
6781
|
"drifted",
|
|
6782
|
-
"undecided"
|
|
6783
|
-
"cannotTell"
|
|
6782
|
+
"undecided"
|
|
6784
6783
|
]);
|
|
6785
6784
|
/**
|
|
6786
|
-
* Axis 2: what happened the last time this spec ran
|
|
6787
|
-
*
|
|
6788
|
-
* (`lastRun.deployedSha`), and collapsing the two is what left the old
|
|
6789
|
-
* single-axis state unable to tell a red spec from an up-to-date one.
|
|
6785
|
+
* Axis 2: what happened the last time this spec ran, and whether that result
|
|
6786
|
+
* still covers what is deployed.
|
|
6790
6787
|
*/
|
|
6791
6788
|
const ExecutionStateSchema = z.enum([
|
|
6792
6789
|
"passed",
|
|
6793
6790
|
"failed",
|
|
6791
|
+
"stale",
|
|
6794
6792
|
"neverRun"
|
|
6795
6793
|
]);
|
|
6796
6794
|
/**
|
|
@@ -6803,16 +6801,15 @@ const SpecVerdictSchema = z.enum([
|
|
|
6803
6801
|
"inProgress",
|
|
6804
6802
|
"needsRepair",
|
|
6805
6803
|
"rerunNeeded",
|
|
6806
|
-
"verified"
|
|
6807
|
-
"unanswerable"
|
|
6804
|
+
"verified"
|
|
6808
6805
|
]);
|
|
6809
6806
|
/**
|
|
6810
|
-
*
|
|
6811
|
-
*
|
|
6812
|
-
*
|
|
6807
|
+
* Which hole in the deploy log made the hub assume a deploy reached this spec
|
|
6808
|
+
* (ADR-0014). Not a state: the verdict is already decided by the time one of
|
|
6809
|
+
* these is attached, and it exists so the view can say *why* a spec is pending
|
|
6810
|
+
* instead of leaving the reader to guess.
|
|
6813
6811
|
*/
|
|
6814
6812
|
const RerunUnknownReasonSchema = z.enum([
|
|
6815
|
-
"notEvaluated",
|
|
6816
6813
|
"noSelectionInRange",
|
|
6817
6814
|
"selectionUnknown",
|
|
6818
6815
|
"noDeployLog",
|
|
@@ -6874,7 +6871,8 @@ const SpecRerunSchema = z.object({
|
|
|
6874
6871
|
audit: AuditStateSchema,
|
|
6875
6872
|
execution: ExecutionStateSchema,
|
|
6876
6873
|
driftLabel: DriftLabelSchema.exclude(["UNKNOWN"]).optional(),
|
|
6877
|
-
|
|
6874
|
+
auditAssumedReached: RerunUnknownReasonSchema.optional(),
|
|
6875
|
+
executionAssumedReached: RerunUnknownReasonSchema.optional(),
|
|
6878
6876
|
heldBy: SpecLockSchema.nullable(),
|
|
6879
6877
|
lastRun: SpecLedgerEntrySchema.nullable(),
|
|
6880
6878
|
lastGreen: SpecLedgerEntrySchema.nullable(),
|
|
@@ -6884,9 +6882,10 @@ const SpecRerunSchema = z.object({
|
|
|
6884
6882
|
});
|
|
6885
6883
|
/**
|
|
6886
6884
|
* Why this spec does or does not need auditing. Everything but `current` and
|
|
6887
|
-
* `held` audits
|
|
6888
|
-
*
|
|
6889
|
-
*
|
|
6885
|
+
* `held` audits. `held` is not an answer about freshness at all — another job
|
|
6886
|
+
* is on it, so this one skips it and asks again next cycle. `cannotTell`
|
|
6887
|
+
* survives here because it is a real explanation of why an audit is owed; the
|
|
6888
|
+
* re-run axis folds it into `due` (ADR-0014).
|
|
6890
6889
|
*/
|
|
6891
6890
|
const AuditNeedSchema = z.object({
|
|
6892
6891
|
because: z.enum([
|
|
@@ -7001,12 +7000,13 @@ const CreateLearningJobRequestSchema = z.object({
|
|
|
7001
7000
|
* makes either flag dangerous.
|
|
7002
7001
|
*/
|
|
7003
7002
|
/**
|
|
7004
|
-
* The release whose hub serves these endpoints in their current shape
|
|
7005
|
-
*
|
|
7006
|
-
* 404s (audit-need, which did not exist) or
|
|
7007
|
-
* rejects (re-run, whose
|
|
7003
|
+
* The release whose hub serves these endpoints in their current shape — the
|
|
7004
|
+
* newest reshape of either, since one number is what a reader can act on. An
|
|
7005
|
+
* older hub either 404s (audit-need, which did not exist before 1.16) or
|
|
7006
|
+
* answers a vocabulary the caller now rejects (re-run, whose verdict values
|
|
7007
|
+
* changed in 1.20).
|
|
7008
7008
|
*/
|
|
7009
|
-
const MIN_HUB_VERSION = "1.
|
|
7009
|
+
const MIN_HUB_VERSION = "1.20";
|
|
7010
7010
|
/**
|
|
7011
7011
|
* Which of the two 404s this was. The handlers answer `no_perspectives` when
|
|
7012
7012
|
* the route exists but the project has no document; any other code on a 404
|
|
@@ -7033,7 +7033,7 @@ function requireHubProfile(flag, profile, question) {
|
|
|
7033
7033
|
function rankedOrder(rank) {
|
|
7034
7034
|
return Object.keys(rank).sort((a, b) => rank[a] - rank[b]);
|
|
7035
7035
|
}
|
|
7036
|
-
/** "3 rerunNeeded, 1
|
|
7036
|
+
/** "3 rerunNeeded, 1 inProgress, 12 verified" — every offered spec accounted for. */
|
|
7037
7037
|
function formatCounts(order, counts) {
|
|
7038
7038
|
return order.filter((key) => counts.has(key)).map((key) => `${counts.get(key)} ${key}`).join(", ");
|
|
7039
7039
|
}
|
|
@@ -7077,39 +7077,47 @@ async function fetchRerunReport(hubCtx, profile) {
|
|
|
7077
7077
|
const SUMMARY_ORDER$1 = rankedOrder({
|
|
7078
7078
|
needsRepair: 0,
|
|
7079
7079
|
rerunNeeded: 1,
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
verified: 4
|
|
7080
|
+
inProgress: 2,
|
|
7081
|
+
verified: 3
|
|
7083
7082
|
});
|
|
7084
7083
|
/**
|
|
7085
7084
|
* Narrow `specs` to the ones the hub says are worth running.
|
|
7086
7085
|
*
|
|
7087
|
-
* `rerunNeeded` is always selected, and
|
|
7088
|
-
*
|
|
7089
|
-
*
|
|
7090
|
-
*
|
|
7091
|
-
*
|
|
7092
|
-
*
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
*/
|
|
7096
|
-
function selectSpecsNeedingRerun(specs, report, opts) {
|
|
7086
|
+
* `rerunNeeded` is always selected, and it now covers everything the hub could
|
|
7087
|
+
* not place: a spec that never ran, and one whose currency the deploy log
|
|
7088
|
+
* cannot vouch for, are both as uncovered as one a deploy demonstrably
|
|
7089
|
+
* invalidated (ADR-0014). `needsRepair`, `inProgress` and `verified` are never
|
|
7090
|
+
* selected: running them repairs nothing, races something already in flight,
|
|
7091
|
+
* or repeats work that is still current.
|
|
7092
|
+
*/
|
|
7093
|
+
function selectSpecsNeedingRerun(specs, report) {
|
|
7097
7094
|
const counts = /* @__PURE__ */ new Map();
|
|
7098
7095
|
const selected = [];
|
|
7099
|
-
let excludedUnanswerable = 0;
|
|
7100
7096
|
let excludedInProgress = 0;
|
|
7097
|
+
let excludedUnknownToHub = 0;
|
|
7098
|
+
let excludedAssumedReached = 0;
|
|
7099
|
+
const assumedReachedReasons = /* @__PURE__ */ new Set();
|
|
7101
7100
|
for (const spec of specs) {
|
|
7102
|
-
const
|
|
7101
|
+
const entry = report.specs[specKey(spec)];
|
|
7102
|
+
const verdict = entry?.verdict ?? "inProgress";
|
|
7103
7103
|
counts.set(verdict, (counts.get(verdict) ?? 0) + 1);
|
|
7104
|
-
if (verdict === "rerunNeeded"
|
|
7105
|
-
else if (verdict === "
|
|
7106
|
-
|
|
7104
|
+
if (verdict === "rerunNeeded") selected.push(spec);
|
|
7105
|
+
else if (verdict === "inProgress") {
|
|
7106
|
+
excludedInProgress++;
|
|
7107
|
+
if (!entry) excludedUnknownToHub++;
|
|
7108
|
+
else if (entry.auditAssumedReached) {
|
|
7109
|
+
excludedAssumedReached++;
|
|
7110
|
+
assumedReachedReasons.add(entry.auditAssumedReached);
|
|
7111
|
+
}
|
|
7112
|
+
}
|
|
7107
7113
|
}
|
|
7108
7114
|
return {
|
|
7109
7115
|
selected,
|
|
7110
7116
|
summary: formatCounts(SUMMARY_ORDER$1, counts),
|
|
7111
|
-
|
|
7112
|
-
|
|
7117
|
+
excludedInProgress,
|
|
7118
|
+
excludedUnknownToHub,
|
|
7119
|
+
excludedAssumedReached,
|
|
7120
|
+
excludedAssumedReachedReasons: [...assumedReachedReasons]
|
|
7113
7121
|
};
|
|
7114
7122
|
}
|
|
7115
7123
|
//#endregion
|
|
@@ -8672,7 +8680,7 @@ const promptRm = new Command("rm").description("Delete a prompt from the hub.").
|
|
|
8672
8680
|
info(`deleted prompt "${name}" from the hub`);
|
|
8673
8681
|
}));
|
|
8674
8682
|
const promptCommand = new Command("prompt").description("Manage prompt assets (per-flow user/agent guidance, triage/audit user guidance, learned calibration prompts) stored on the hub (fetched automatically by `ccqa run` / `ccqa audit` at run time).").addCommand(promptPush).addCommand(promptLs).addCommand(promptRm);
|
|
8675
|
-
const deployRecord = new Command("record").description("Tell the hub what a deploy shipped, so it can answer which specs need a re-run (`ccqa run --only-hub-rerun-needed`). Run this from the deploy job, after the deploy succeeds. The changed paths are computed locally with a two-dot `git diff <previous> <sha>`; a job that has only curl and git can POST the same body directly (see docs/hub.md).").requiredOption("--profile <name>", "Environment this deploy landed in (e.g. 'stg'). Required: dev and stg sit at different commits, so the deploy log is per-profile.").requiredOption("--sha <sha>", "Commit that was deployed.").option("--previous <sha>", "Commit this deploy replaced.
|
|
8683
|
+
const deployRecord = new Command("record").description("Tell the hub what a deploy shipped, so it can answer which specs need a re-run (`ccqa run --only-hub-rerun-needed`). Run this from the deploy job, after the deploy succeeds. The changed paths are computed locally with a two-dot `git diff <previous> <sha>`; a job that has only curl and git can POST the same body directly (see docs/hub.md).").requiredOption("--profile <name>", "Environment this deploy landed in (e.g. 'stg'). Required: dev and stg sit at different commits, so the deploy log is per-profile.").requiredOption("--sha <sha>", "Commit that was deployed.").option("--previous <sha>", "Commit this deploy replaced. Omit it and the hub's current log head is used — the normal case, recording no discontinuity. Pass a sha that differs from the head and the hub records one (gapBefore) in the chain: use this for a first record with a real baseline, or to re-anchor a head that no longer matches reality. With no head and nothing passed, there's nothing to diff against: changedPaths is unset and the spec selection is skipped.").option("--ref <ref>", "Ref that was deployed (branch or tag). Recorded for display only.").option("--no-select-specs", "Record the deploy without deciding which specs it reaches. The entry then becomes a hole in the range — every spec behind it is assumed reached rather than being cleared, and nothing can fill it in later, since the hub has no checkout to diff. Only pass this when no Claude credential is available; it costs one model call to leave the range clearable.").option("-m, --model <name>", "Model for the spec selection. Claude alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option(...hubUrlOption).option(...hubTokenOption).option("--project <name>", "Project whose deploy log this entry joins. Defaults to the current directory's name.").option("--cwd <path>", "Directory the git diff and the default --project name are resolved against.").action(withHubErrors(async (opts) => {
|
|
8676
8684
|
const cwd = resolveCwd(opts.cwd);
|
|
8677
8685
|
const project = resolveProject(opts);
|
|
8678
8686
|
const hub = connect(opts);
|
|
@@ -8695,9 +8703,9 @@ const deployRecord = new Command("record").description("Tell the hub what a depl
|
|
|
8695
8703
|
meta("previous", previous ? previous.slice(0, 12) : "(none)");
|
|
8696
8704
|
meta("changed paths", changedPaths === null ? "(not reported)" : String(changedPaths.length));
|
|
8697
8705
|
if (opts.selectSpecs !== false) meta("selection", describeSelection(selection, diff !== null));
|
|
8698
|
-
if (entry.gapBefore) warn("this deploy does not chain onto the log head, so a gap is recorded — specs whose baseline sits behind it
|
|
8706
|
+
if (entry.gapBefore) warn("this deploy does not chain onto the log head, so a gap is recorded — specs whose baseline sits behind it are assumed reached rather than cleared to 'verified'");
|
|
8699
8707
|
if (selection && !entry.hasSelection) {
|
|
8700
|
-
error("the hub recorded this deploy but could not store its spec selection —
|
|
8708
|
+
error("the hub recorded this deploy but could not store its spec selection — every spec behind it is assumed reached from here on, and nothing fills it in later. Re-record this deploy.");
|
|
8701
8709
|
process.exit(1);
|
|
8702
8710
|
}
|
|
8703
8711
|
info(`recorded deploy #${entry.index}`);
|
|
@@ -12193,7 +12201,6 @@ async function executeRun(targets, opts) {
|
|
|
12193
12201
|
const filtering = Boolean(opts.onlyAffectedBy || opts.onlyHubRerunNeeded);
|
|
12194
12202
|
if (filtering && targets.length > 0) throw new RunUsageError("a --only-* filter and an explicit spec target cannot be combined");
|
|
12195
12203
|
const rerunProfile = opts.onlyHubRerunNeeded === true ? requireRerunProfile(opts.hubProfile) : null;
|
|
12196
|
-
if (opts.onlyHubRerunNeededWithUnknown && rerunProfile === null) warn("--only-hub-rerun-needed-with-unknown is ignored: it only applies to --only-hub-rerun-needed");
|
|
12197
12204
|
const forExecution = opts.dryRun !== true;
|
|
12198
12205
|
const cwd = opts.cwd ?? process.cwd();
|
|
12199
12206
|
const wantsLastGreen = opts.onFailExplain === true && opts.onFailExplainBase === void 0;
|
|
@@ -12281,13 +12288,17 @@ async function executeRun(targets, opts) {
|
|
|
12281
12288
|
let specs = dedupeSpecs((await Promise.all((targets.length ? targets : [void 0]).map((t) => resolveSpecTargets(t, enumerateAll, cwd)))).flat());
|
|
12282
12289
|
if (filtering) {
|
|
12283
12290
|
const before = specs.length;
|
|
12284
|
-
let unanswerable = 0;
|
|
12285
12291
|
let inProgress = 0;
|
|
12292
|
+
let unknownToHub = 0;
|
|
12293
|
+
let assumedReached = 0;
|
|
12294
|
+
let assumedReachedReasons = [];
|
|
12286
12295
|
if (rerunReport) {
|
|
12287
|
-
const selection = selectSpecsNeedingRerun(specs, rerunReport
|
|
12296
|
+
const selection = selectSpecsNeedingRerun(specs, rerunReport);
|
|
12288
12297
|
specs = selection.selected;
|
|
12289
|
-
unanswerable = selection.excludedUnanswerable;
|
|
12290
12298
|
inProgress = selection.excludedInProgress;
|
|
12299
|
+
unknownToHub = selection.excludedUnknownToHub;
|
|
12300
|
+
assumedReached = selection.excludedAssumedReached;
|
|
12301
|
+
assumedReachedReasons = selection.excludedAssumedReachedReasons;
|
|
12291
12302
|
meta("stale-base", `deploy ${rerunReport.deployHead.sha.slice(0, 12)} (profile ${rerunReport.profile})`);
|
|
12292
12303
|
meta("stale-states", selection.summary);
|
|
12293
12304
|
}
|
|
@@ -12297,9 +12308,11 @@ async function executeRun(targets, opts) {
|
|
|
12297
12308
|
...opts.model ? { model: opts.model } : {}
|
|
12298
12309
|
})).specs;
|
|
12299
12310
|
meta("selected", `${specs.length} of ${before} spec${before === 1 ? "" : "s"}`);
|
|
12300
|
-
if (specs.length === 0 &&
|
|
12301
|
-
if (
|
|
12302
|
-
if (
|
|
12311
|
+
if (specs.length === 0 && inProgress > 0) {
|
|
12312
|
+
if (unknownToHub > 0) hint(`${unknownToHub} spec(s) were excluded because the hub's perspectives document has never heard of them; run \`ccqa perspectives\` to register them`);
|
|
12313
|
+
if (assumedReached > 0) hint(`${assumedReached} spec(s) were excluded because the audit ran at a commit the deploy log cannot place (${assumedReachedReasons.join(", ")}); re-running \`ccqa audit --only-hub-audit-needed\` at the same commit will not clear it — record the deploy (\`ccqa hub deploy record\`) so the log can place it, or select with --only-affected-by <ref> to make progress meanwhile`);
|
|
12314
|
+
const explained = unknownToHub + assumedReached;
|
|
12315
|
+
if (explained < inProgress) hint(`${inProgress - explained} spec(s) were excluded because the audit has not answered for the deployed commit yet; run \`ccqa audit --only-hub-audit-needed --report-to-hub\` first`);
|
|
12303
12316
|
throw new RunUsageError("nothing was selected and no spec was cleared to run: exiting non-zero rather than reporting a green run that verified nothing");
|
|
12304
12317
|
}
|
|
12305
12318
|
}
|
|
@@ -13053,7 +13066,7 @@ function installTeardownSignalHandlers(teardown) {
|
|
|
13053
13066
|
}
|
|
13054
13067
|
//#endregion
|
|
13055
13068
|
//#region src/cli/run.ts
|
|
13056
|
-
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs, on any target. Agent-browser specs replay the recorded test.spec.ts under vitest (default), or, with spec.yaml `mode: live`, have Claude drive agent-browser live per step. External-target specs (playwright, runn) run through the target's configured `runCommand`. A structured report (report.json + evidence) is always written; use --report-to-hub to also stream it to a hub.").optionsGroup("Which specs to run:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Cannot be combined with an explicit spec id.").option("--only-hub-rerun-needed", "Only specs the hub answers `rerunNeeded` for: the audit cleared them, and their last result does not cover what is deployed. A spec whose audit has not caught up answers `inProgress`, and one the audit rejected or whose last run failed answers `needsRepair`; neither is taken, because running them races the audit or repairs nothing. No git diff involved. Requires a hub connection and --hub-profile.").option("--
|
|
13069
|
+
const runCommand = addHubOptions(addProfileOption(addLanguageOption(new Command("run").argument("[targets...]", "Specs to run, space-separated: each '<feature>/<spec>', '<feature>', or omit for all. Duplicates are de-duped.").description("Run specs, on any target. Agent-browser specs replay the recorded test.spec.ts under vitest (default), or, with spec.yaml `mode: live`, have Claude drive agent-browser live per step. External-target specs (playwright, runn) run through the target's configured `runCommand`. A structured report (report.json + evidence) is always written; use --report-to-hub to also stream it to a hub.").optionsGroup("Which specs to run:").option("--only-affected-by <ref>", "Only specs `ccqa select-specs` judges reached by the diff against <ref> (e.g. origin/main). In pull_request CI, pass $GITHUB_BASE_REF. Cannot be combined with an explicit spec id.").option("--only-hub-rerun-needed", "Only specs the hub answers `rerunNeeded` for: the audit cleared them, and their last result does not cover what is deployed — including every spec the deploy log cannot place, which is assumed reached rather than skipped. A spec whose audit has not caught up answers `inProgress`, and one the audit rejected or whose last run failed answers `needsRepair`; neither is taken, because running them races the audit or repairs nothing. No git diff involved. Requires a hub connection and --hub-profile.").option("--dry-run", "Print the specs this invocation would run, then exit 0 without executing anything and without writing a report. Works with every selection flag.").optionsGroup("How to run them:").option("--concurrency <n>", "Run up to N specs in parallel within each phase (deterministic / external-target / live), never across phases. Default 1 (sequential). Specs in the same `serialGroups` entry of .ccqa/config.yaml still take turns. Live specs each get an isolated agent-browser session; high values spawn many headed Chrome instances.", parseConcurrency$1, 1).option("-m, --model <name>", "Claude model alias ('sonnet'|'opus'|'haiku') or full ID. Overrides CCQA_MODEL.").option("--live-step-retry <n>", "(live only) Retry each failed step up to N more times before recording failure. This retries a step, not the whole spec — see --on-fail-explain-rerun for that.", (raw) => {
|
|
13057
13070
|
const n = Number(raw);
|
|
13058
13071
|
if (!Number.isFinite(n) || n < 0 || Math.floor(n) !== n) throw new Error(`--live-step-retry must be a non-negative integer, got "${raw}"`);
|
|
13059
13072
|
return n;
|
|
@@ -15466,9 +15479,9 @@ function buildRange(log, touchIndex) {
|
|
|
15466
15479
|
}
|
|
15467
15480
|
function freshness(baselineSha, key, range) {
|
|
15468
15481
|
const { log, positionBySha, touchIndex } = range;
|
|
15469
|
-
if (log.entries.length === 0) return unanswerable
|
|
15482
|
+
if (log.entries.length === 0) return unanswerable("noDeployLog");
|
|
15470
15483
|
const baselinePos = positionBySha.get(baselineSha);
|
|
15471
|
-
if (baselinePos === void 0) return unanswerable
|
|
15484
|
+
if (baselinePos === void 0) return unanswerable("deployedShaNotInLog");
|
|
15472
15485
|
const baselineIndex = log.entries[baselinePos].index;
|
|
15473
15486
|
const touch = touchIndex[key];
|
|
15474
15487
|
const touched = touch?.needed;
|
|
@@ -15480,9 +15493,9 @@ function freshness(baselineSha, key, range) {
|
|
|
15480
15493
|
touchedByDeploy: entry ? deployRef(entry) : null
|
|
15481
15494
|
};
|
|
15482
15495
|
}
|
|
15483
|
-
if (range.gapFromPos[baselinePos + 1]) return unanswerable
|
|
15484
|
-
if (range.noSelectionFromPos[baselinePos + 1]) return unanswerable
|
|
15485
|
-
if (touch?.undecidedIndex !== void 0 && touch.undecidedIndex > baselineIndex) return unanswerable
|
|
15496
|
+
if (range.gapFromPos[baselinePos + 1]) return unanswerable("gapInRange");
|
|
15497
|
+
if (range.noSelectionFromPos[baselinePos + 1]) return unanswerable("noSelectionInRange");
|
|
15498
|
+
if (touch?.undecidedIndex !== void 0 && touch.undecidedIndex > baselineIndex) return unanswerable("selectionUnknown");
|
|
15486
15499
|
return { kind: "current" };
|
|
15487
15500
|
}
|
|
15488
15501
|
function deployRef(entry) {
|
|
@@ -15492,7 +15505,7 @@ function deployRef(entry) {
|
|
|
15492
15505
|
at: entry.at
|
|
15493
15506
|
};
|
|
15494
15507
|
}
|
|
15495
|
-
function unanswerable
|
|
15508
|
+
function unanswerable(reason) {
|
|
15496
15509
|
return {
|
|
15497
15510
|
kind: "unanswerable",
|
|
15498
15511
|
reason
|
|
@@ -17778,7 +17791,6 @@ function createReleaseLocksHandler(storage) {
|
|
|
17778
17791
|
function computeRerun(input) {
|
|
17779
17792
|
const { specs, ledger, log, touchIndex, drift, locks, now } = input;
|
|
17780
17793
|
const range = buildRange(log, touchIndex);
|
|
17781
|
-
const nothingRecorded = log.entries.length === 0 && Object.keys(ledger.run).length === 0 && Object.keys(ledger.green).length === 0;
|
|
17782
17794
|
const out = {};
|
|
17783
17795
|
for (const spec of specs) {
|
|
17784
17796
|
const coords = {
|
|
@@ -17787,16 +17799,12 @@ function computeRerun(input) {
|
|
|
17787
17799
|
lastRed: ledger.red[spec.key] ?? null
|
|
17788
17800
|
};
|
|
17789
17801
|
const audit = auditState(drift, spec.key, range);
|
|
17790
|
-
const execution = executionState(coords);
|
|
17802
|
+
const execution = executionState(coords, (sha) => freshness(sha, spec.key, range));
|
|
17791
17803
|
const held = heldBy(locks, spec.key, now);
|
|
17792
|
-
const derived = nothingRecorded ? {
|
|
17793
|
-
verdict: "unanswerable",
|
|
17794
|
-
reason: "notEvaluated"
|
|
17795
|
-
} : decide(audit, execution, held, coords.lastRun, (sha) => freshness(sha, spec.key, range));
|
|
17796
17804
|
out[spec.key] = {
|
|
17797
|
-
|
|
17805
|
+
verdict: decide(audit.audit, execution.execution, held),
|
|
17798
17806
|
...audit,
|
|
17799
|
-
execution,
|
|
17807
|
+
...execution,
|
|
17800
17808
|
heldBy: held,
|
|
17801
17809
|
...coords
|
|
17802
17810
|
};
|
|
@@ -17807,6 +17815,10 @@ function computeRerun(input) {
|
|
|
17807
17815
|
* Axis 1, derived from the same freshness answer `--only-hub-audit-needed`
|
|
17808
17816
|
* reads. The label only speaks once the audit is known to be current: a
|
|
17809
17817
|
* verdict about an older commit says nothing about the one running now.
|
|
17818
|
+
*
|
|
17819
|
+
* A commit the log cannot place is `due` like any other: a deploy we cannot
|
|
17820
|
+
* rule out is treated as having landed (ADR-0014). The hole is kept as an
|
|
17821
|
+
* annotation so the answer stays explicable.
|
|
17810
17822
|
*/
|
|
17811
17823
|
function auditState(drift, key, range) {
|
|
17812
17824
|
const need = auditNeed(drift, key, range);
|
|
@@ -17814,8 +17826,8 @@ function auditState(drift, key, range) {
|
|
|
17814
17826
|
case "neverAudited":
|
|
17815
17827
|
case "deployReached": return { audit: "due" };
|
|
17816
17828
|
case "cannotTell": return {
|
|
17817
|
-
audit: "
|
|
17818
|
-
...need.reason ? {
|
|
17829
|
+
audit: "due",
|
|
17830
|
+
...need.reason ? { auditAssumedReached: need.reason } : {}
|
|
17819
17831
|
};
|
|
17820
17832
|
case "current": {
|
|
17821
17833
|
const label = drift.specs[key].label;
|
|
@@ -17833,56 +17845,57 @@ function auditState(drift, key, range) {
|
|
|
17833
17845
|
}
|
|
17834
17846
|
}
|
|
17835
17847
|
/**
|
|
17836
|
-
* Axis 2
|
|
17837
|
-
*
|
|
17838
|
-
*
|
|
17839
|
-
|
|
17840
|
-
|
|
17841
|
-
|
|
17842
|
-
|
|
17843
|
-
|
|
17848
|
+
* Axis 2: how the last run ended, and whether a deploy has overtaken it. The
|
|
17849
|
+
* red bucket is compared by run id rather than by timestamp because both
|
|
17850
|
+
* buckets advance from the same terminal-run trigger, so the run that wrote
|
|
17851
|
+
* `run` wrote exactly one of `green` or `red`.
|
|
17852
|
+
*
|
|
17853
|
+
* `failed` outranks `stale`: a red result is current information whatever has
|
|
17854
|
+
* deployed since, and re-running it teaches nothing until someone repairs it.
|
|
17855
|
+
*/
|
|
17856
|
+
function executionState(coords, since) {
|
|
17857
|
+
const { lastRun, lastRed } = coords;
|
|
17858
|
+
if (!lastRun) return { execution: "neverRun" };
|
|
17859
|
+
if (lastRed && lastRed.runId === lastRun.runId) return { execution: "failed" };
|
|
17860
|
+
if (lastRun.deployedShaAmbiguous) return assumedReached("ambiguousDeployedSha");
|
|
17861
|
+
if (!lastRun.deployedSha) return assumedReached("unknownDeployedSha");
|
|
17862
|
+
const answer = since(lastRun.deployedSha);
|
|
17863
|
+
if (answer.kind === "unanswerable") return assumedReached(answer.reason);
|
|
17864
|
+
if (answer.kind === "current") return { execution: "passed" };
|
|
17865
|
+
return {
|
|
17866
|
+
execution: "stale",
|
|
17867
|
+
...answer.touchedBy ? { touchedBy: answer.touchedBy } : {},
|
|
17868
|
+
touchedByDeploy: answer.touchedByDeploy
|
|
17869
|
+
};
|
|
17870
|
+
}
|
|
17871
|
+
function assumedReached(reason) {
|
|
17872
|
+
return {
|
|
17873
|
+
execution: "stale",
|
|
17874
|
+
executionAssumedReached: reason
|
|
17875
|
+
};
|
|
17844
17876
|
}
|
|
17845
17877
|
/**
|
|
17846
|
-
* The derived answer
|
|
17847
|
-
* spec
|
|
17848
|
-
*
|
|
17878
|
+
* The derived answer: a total function of the two axes plus whether a job
|
|
17879
|
+
* holds the spec. Nothing else is consulted — an axis that needed a third
|
|
17880
|
+
* input to be read is an axis that cannot be shown next to the verdict as its
|
|
17881
|
+
* reason.
|
|
17849
17882
|
*/
|
|
17850
|
-
function decide(audit, execution, held
|
|
17851
|
-
if (held) return
|
|
17852
|
-
switch (audit
|
|
17853
|
-
case "
|
|
17854
|
-
case "due": return { verdict: "inProgress" };
|
|
17883
|
+
function decide(audit, execution, held) {
|
|
17884
|
+
if (held) return "inProgress";
|
|
17885
|
+
switch (audit) {
|
|
17886
|
+
case "due": return "inProgress";
|
|
17855
17887
|
case "drifted":
|
|
17856
|
-
case "undecided": return
|
|
17888
|
+
case "undecided": return "needsRepair";
|
|
17857
17889
|
case "clean": break;
|
|
17858
|
-
default: {
|
|
17859
|
-
const unreachable = audit.audit;
|
|
17860
|
-
throw new Error(`unhandled audit state: ${String(unreachable)}`);
|
|
17861
|
-
}
|
|
17890
|
+
default: throw new Error(`unhandled audit state: ${String(audit)}`);
|
|
17862
17891
|
}
|
|
17863
17892
|
switch (execution) {
|
|
17864
|
-
case "failed": return
|
|
17865
|
-
case "
|
|
17866
|
-
case "neverRun":
|
|
17893
|
+
case "failed": return "needsRepair";
|
|
17894
|
+
case "stale":
|
|
17895
|
+
case "neverRun": return "rerunNeeded";
|
|
17896
|
+
case "passed": return "verified";
|
|
17867
17897
|
default: throw new Error(`unhandled execution state: ${String(execution)}`);
|
|
17868
17898
|
}
|
|
17869
|
-
if (!lastRun) return { verdict: "rerunNeeded" };
|
|
17870
|
-
if (lastRun.deployedShaAmbiguous) return unanswerable("ambiguousDeployedSha");
|
|
17871
|
-
if (!lastRun.deployedSha) return unanswerable("unknownDeployedSha");
|
|
17872
|
-
const answer = since(lastRun.deployedSha);
|
|
17873
|
-
if (answer.kind === "unanswerable") return unanswerable(answer.reason);
|
|
17874
|
-
if (answer.kind === "current") return { verdict: "verified" };
|
|
17875
|
-
return {
|
|
17876
|
-
verdict: "rerunNeeded",
|
|
17877
|
-
...answer.touchedBy ? { touchedBy: answer.touchedBy } : {},
|
|
17878
|
-
touchedByDeploy: answer.touchedByDeploy
|
|
17879
|
-
};
|
|
17880
|
-
}
|
|
17881
|
-
function unanswerable(reason) {
|
|
17882
|
-
return {
|
|
17883
|
-
verdict: "unanswerable",
|
|
17884
|
-
reason
|
|
17885
|
-
};
|
|
17886
17899
|
}
|
|
17887
17900
|
//#endregion
|
|
17888
17901
|
//#region src/hub/api/handlers/rerun.ts
|
|
@@ -18601,17 +18614,30 @@ const HTML_BODY = `
|
|
|
18601
18614
|
<div id="persp-body" hidden>
|
|
18602
18615
|
<div class="ov" id="persp-ov"></div>
|
|
18603
18616
|
<div class="note info persp-note" id="persp-rerun-note" hidden></div>
|
|
18604
|
-
<div class="note info persp-note" id="persp-drift-note" hidden></div>
|
|
18605
18617
|
<div class="toolbar">
|
|
18606
18618
|
<label class="search"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg><input id="persp-q" type="search" data-i18n-ph="perspectives.search" aria-label="Search cases"></label>
|
|
18607
18619
|
<!-- Each chip carries the count of what it would leave behind, so
|
|
18608
18620
|
data-i18n sits on the inner label span: applyStaticI18n swaps
|
|
18609
|
-
textContent, which on the button would delete the count.
|
|
18610
|
-
|
|
18611
|
-
|
|
18612
|
-
|
|
18613
|
-
<
|
|
18614
|
-
|
|
18621
|
+
textContent, which on the button would delete the count. Two
|
|
18622
|
+
labelled groups rather than one row: mode and verdict are
|
|
18623
|
+
different questions, and mixing their chips together read as
|
|
18624
|
+
one. -->
|
|
18625
|
+
<div class="fgroup">
|
|
18626
|
+
<span class="fgroup-label" data-i18n="perspectives.filter.group.mode">Mode</span>
|
|
18627
|
+
<button class="fchip" data-f="all" aria-pressed="true" type="button"><span data-i18n="perspectives.filter.all">All</span><span class="fcount"></span></button>
|
|
18628
|
+
<button class="fchip" data-f="deterministic" aria-pressed="false" type="button"><span data-i18n="perspectives.filter.deterministic">Deterministic</span><span class="fcount"></span></button>
|
|
18629
|
+
<button class="fchip" data-f="live" aria-pressed="false" type="button"><span data-i18n="perspectives.filter.live">Live</span><span class="fcount"></span></button>
|
|
18630
|
+
</div>
|
|
18631
|
+
<!-- Same words as the 判定 column (perspectives.rerun.state.*) and
|
|
18632
|
+
the same group label as its header (perspectives.col.verdict)
|
|
18633
|
+
— a filter chip must never coin its own name for a verdict. -->
|
|
18634
|
+
<div class="fgroup" id="persp-verdict-chips" hidden>
|
|
18635
|
+
<span class="fgroup-label" data-i18n="perspectives.col.verdict">Verdict</span>
|
|
18636
|
+
<button class="fchip" id="persp-chip-needsrepair" data-f="needsRepair" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.needsRepair">Needs repair</span><span class="fcount"></span></button>
|
|
18637
|
+
<button class="fchip" id="persp-chip-rerunneeded" data-f="rerunNeeded" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.rerunNeeded">Re-run needed</span><span class="fcount"></span></button>
|
|
18638
|
+
<button class="fchip" id="persp-chip-inprogress" data-f="inProgress" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.inProgress">In progress</span><span class="fcount"></span></button>
|
|
18639
|
+
<button class="fchip" id="persp-chip-verified" data-f="verified" aria-pressed="false" type="button"><span data-i18n="perspectives.rerun.state.verified">Verified</span><span class="fcount"></span></button>
|
|
18640
|
+
</div>
|
|
18615
18641
|
<div class="spacer"></div>
|
|
18616
18642
|
<span class="muted persp-head" id="persp-deploy-head" hidden></span>
|
|
18617
18643
|
<div class="sw-wrap" id="persp-profile-wrap">
|
|
@@ -18623,7 +18649,7 @@ const HTML_BODY = `
|
|
|
18623
18649
|
</div>
|
|
18624
18650
|
</div>
|
|
18625
18651
|
<div class="tblcard"><div class="table-wrap"><table>
|
|
18626
|
-
<thead><tr><th data-i18n="perspectives.col.case">Case</th><th data-i18n="perspectives.col.mode">Mode</th><th id="persp-th-verdict" data-i18n="perspectives.col.verdict" hidden>
|
|
18652
|
+
<thead><tr><th data-i18n="perspectives.col.case">Case</th><th data-i18n="perspectives.col.mode">Mode</th><th id="persp-th-verdict" data-i18n="perspectives.col.verdict" hidden>Verdict</th><th id="persp-th-run" data-i18n="perspectives.col.run" hidden>Execution</th><th id="persp-th-audit" data-i18n="perspectives.col.audit" hidden>Audit</th><th></th></tr></thead>
|
|
18627
18653
|
<tbody id="persp-tbody"></tbody>
|
|
18628
18654
|
</table></div></div>
|
|
18629
18655
|
<p class="empty-note" id="persp-no-hit" hidden data-i18n="perspectives.noHit">No matching cases.</p>
|
|
@@ -19294,13 +19320,15 @@ const CSS = `
|
|
|
19294
19320
|
existing badge/chip primitives above, so no new tokens are needed.
|
|
19295
19321
|
|
|
19296
19322
|
The summary row answers the question this tab exists for — which cases
|
|
19297
|
-
need
|
|
19298
|
-
|
|
19299
|
-
|
|
19323
|
+
need attention — as one inventory line plus one bar per axis: verdict,
|
|
19324
|
+
execution, audit, the same three groupings the table's columns show. The
|
|
19325
|
+
mode and recorded-ness counts moved onto the filter chips, which is
|
|
19326
|
+
where a count says something actionable. */
|
|
19300
19327
|
.ov { display: flex; flex-direction: column; gap: 10px; padding: 14px 18px; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); margin-bottom: 16px; }
|
|
19301
19328
|
.ov-inv { font-size: 13px; color: var(--muted); }
|
|
19302
19329
|
.ov-inv b { color: var(--fg); font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
19303
|
-
/* One row per overview axis (
|
|
19330
|
+
/* One row per overview axis (verdict, execution, audit): a short label —
|
|
19331
|
+
reusing that axis's own column header text — above its own bar+legend. */
|
|
19304
19332
|
.ov-axis { display: flex; flex-direction: column; gap: 4px; }
|
|
19305
19333
|
.ov-axis-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted-2); }
|
|
19306
19334
|
.rrbar { height: 8px; border-radius: 999px; overflow: hidden; display: flex; background: var(--surface-3); }
|
|
@@ -19308,22 +19336,25 @@ const CSS = `
|
|
|
19308
19336
|
.rrleg span { display: inline-flex; align-items: center; gap: 6px; }
|
|
19309
19337
|
.rrleg i { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
19310
19338
|
.rrleg b { color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
19311
|
-
/* One class per
|
|
19312
|
-
|
|
19313
|
-
|
|
19314
|
-
|
|
19315
|
-
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
.sg-
|
|
19319
|
-
.sg-
|
|
19320
|
-
.sg-
|
|
19321
|
-
.sg-
|
|
19322
|
-
.sg-drift-none, .sg-inprogress { background: var(--muted-2); }
|
|
19339
|
+
/* One class per state, worn by both the bar segment and its legend dot.
|
|
19340
|
+
The execution and audit axes share these colours with the verdict axis
|
|
19341
|
+
rather than inventing two more palettes — same reasoning as the badge
|
|
19342
|
+
classes below. Careful writing in here: a star followed by a slash
|
|
19343
|
+
closes the comment early and silently eats the next rule, and a
|
|
19344
|
+
backtick ends the template literal this CSS lives in. */
|
|
19345
|
+
.sg-rerunneeded, .sg-exec-stale { background: var(--amber-fill); }
|
|
19346
|
+
.sg-needsrepair, .sg-audit-drifted, .sg-exec-failed { background: var(--fail); }
|
|
19347
|
+
.sg-audit-undecided { background: var(--info); }
|
|
19348
|
+
.sg-verified, .sg-audit-clean, .sg-exec-passed { background: var(--pass); }
|
|
19349
|
+
.sg-inprogress, .sg-audit-due, .sg-exec-never { background: var(--muted-2); }
|
|
19323
19350
|
|
|
19324
19351
|
.search { flex: 1; min-width: 200px; max-width: 340px; display: flex; align-items: center; gap: 7px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 0 10px; height: 32px; background: var(--surface); }
|
|
19325
19352
|
.search svg { width: 15px; height: 15px; flex: none; color: var(--muted-2); }
|
|
19326
19353
|
.search input { border: none; outline: none; font: inherit; font-size: 13px; width: 100%; background: transparent; color: var(--fg); }
|
|
19354
|
+
/* A labelled cluster of chips (mode, verdict) rather than one flat row —
|
|
19355
|
+
the label says what question the chips beside it answer. */
|
|
19356
|
+
.fgroup { display: inline-flex; align-items: center; gap: 6px; }
|
|
19357
|
+
.fgroup-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted-2); margin-right: 2px; }
|
|
19327
19358
|
.fchip { border: 1px solid var(--border-strong); background: var(--surface); border-radius: 999px; padding: 5px 12px; font-size: 12.5px; color: var(--muted); }
|
|
19328
19359
|
.fchip[aria-pressed="true"] { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
|
|
19329
19360
|
.fchip .fcount { margin-left: 6px; font-variant-numeric: tabular-nums; color: var(--muted-2); }
|
|
@@ -19335,35 +19366,26 @@ const CSS = `
|
|
|
19335
19366
|
.badge.norec { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
19336
19367
|
.badge.norec .d { background: var(--amber); }
|
|
19337
19368
|
|
|
19338
|
-
/*
|
|
19339
|
-
never be mistaken for rr-
|
|
19340
|
-
dimmed
|
|
19341
|
-
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
.badge.rr-
|
|
19350
|
-
.badge.rr-needed .d, .badge.dr-found .d { background: var(--amber); }
|
|
19351
|
-
.badge.rr-notneeded, .badge.dr-clean { background: var(--pass-bg); color: var(--pass); border-color: var(--pass-border); }
|
|
19352
|
-
.badge.rr-notneeded .d, .badge.dr-clean .d { background: var(--pass); }
|
|
19353
|
-
/* Same blue on both axes: "we cannot say" means the same thing whether the
|
|
19354
|
-
question is re-run or drift, and a reader should not have to relearn it. */
|
|
19355
|
-
.badge.rr-unknown, .badge.dr-unknown { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
|
|
19356
|
-
.badge.rr-unknown .d, .badge.dr-unknown .d { background: var(--info); }
|
|
19357
|
-
.badge.rr-none, .badge.dr-none { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
19358
|
-
.badge.rr-none .d, .badge.dr-none .d { background: var(--muted); }
|
|
19369
|
+
/* Shared by the verdict and audit columns (ADR-0010, ADR-0014): rr-unknown
|
|
19370
|
+
must never be mistaken for a clean rr-none, so it takes the info hue
|
|
19371
|
+
rather than a dimmed grey, and every badge is paired with a .cellsub
|
|
19372
|
+
saying what it rests on. */
|
|
19373
|
+
.badge.rr-needed { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
19374
|
+
.badge.rr-needed .d { background: var(--amber); }
|
|
19375
|
+
.badge.rr-repair { background: var(--fail-bg); color: var(--fail); border-color: var(--fail-border); }
|
|
19376
|
+
.badge.rr-repair .d { background: var(--fail); }
|
|
19377
|
+
.badge.rr-unknown { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
|
|
19378
|
+
.badge.rr-unknown .d { background: var(--info); }
|
|
19379
|
+
.badge.rr-none { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
19380
|
+
.badge.rr-none .d { background: var(--muted); }
|
|
19359
19381
|
.cellsub { display: block; margin-top: 3px; max-width: 260px; color: var(--muted); font-size: 11.5px; line-height: 1.45; }
|
|
19360
19382
|
.graded-mark { color: var(--fg-dim); font-weight: 600; }
|
|
19361
19383
|
.cellsub a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--border-strong); }
|
|
19362
19384
|
.cellsub a:hover { color: var(--fg); }
|
|
19363
19385
|
.persp-note { margin-bottom: 12px; }
|
|
19364
19386
|
.persp-head { font-size: 12px; white-space: nowrap; }
|
|
19365
|
-
/* The Perspectives toolbar carries search +
|
|
19366
|
-
selector, so it wraps instead of overflowing on a narrow window. */
|
|
19387
|
+
/* The Perspectives toolbar carries search + two filter-chip groups + the
|
|
19388
|
+
profile selector, so it wraps instead of overflowing on a narrow window. */
|
|
19367
19389
|
#view-perspectives .toolbar { flex-wrap: wrap; }
|
|
19368
19390
|
.proj-menu.right { left: auto; right: 0; }
|
|
19369
19391
|
.d-note { margin-top: 12px; max-width: 900px; }
|
|
@@ -19510,15 +19532,14 @@ const CLIENT_JS = `
|
|
|
19510
19532
|
"perspectives.search": "Search cases…",
|
|
19511
19533
|
"perspectives.filter.all": "All", "perspectives.filter.deterministic": "Deterministic",
|
|
19512
19534
|
"perspectives.filter.live": "Live",
|
|
19513
|
-
"perspectives.filter.
|
|
19514
|
-
"perspectives.col.verdict": "
|
|
19515
|
-
"perspectives.col.run": "Execution",
|
|
19535
|
+
"perspectives.filter.group.mode": "Mode",
|
|
19536
|
+
"perspectives.col.verdict": "Verdict", "perspectives.col.audit": "Audit",
|
|
19537
|
+
"perspectives.col.run": "Execution",
|
|
19516
19538
|
"perspectives.audit.state.due": "Audit due",
|
|
19517
19539
|
"perspectives.audit.state.clean": "Describes the code",
|
|
19518
19540
|
"perspectives.audit.state.drifted": "Drifted",
|
|
19519
19541
|
"perspectives.audit.state.undecided": "Couldn't tell",
|
|
19520
|
-
"perspectives.
|
|
19521
|
-
"perspectives.run.state.needed": "re-run needed", "perspectives.run.state.unknown": "can't tell",
|
|
19542
|
+
"perspectives.run.state.superseded": "pending",
|
|
19522
19543
|
"perspectives.run.state.failed": "failed", "perspectives.run.state.passed": "passed",
|
|
19523
19544
|
"perspectives.run.state.never": "never run",
|
|
19524
19545
|
"perspectives.col.case": "Case", "perspectives.col.mode": "Mode",
|
|
@@ -19528,7 +19549,6 @@ const CLIENT_JS = `
|
|
|
19528
19549
|
"perspectives.loadFailed": "Loading perspectives failed",
|
|
19529
19550
|
"perspectives.mode.deterministic": "deterministic", "perspectives.mode.live": "live",
|
|
19530
19551
|
"perspectives.ov.cases": "cases", "perspectives.ov.features": "features",
|
|
19531
|
-
"perspectives.ov.axis.rerun": "Re-run", "perspectives.ov.axis.drift": "Drift",
|
|
19532
19552
|
"perspectives.d.preconditions": "Preconditions", "perspectives.d.startScreen": "Start screen",
|
|
19533
19553
|
"perspectives.d.testCondition": "Condition", "perspectives.d.spec": "spec",
|
|
19534
19554
|
"perspectives.note.label": "Note",
|
|
@@ -19537,12 +19557,11 @@ const CLIENT_JS = `
|
|
|
19537
19557
|
"perspectives.note.error": "Could not save — retry",
|
|
19538
19558
|
"perspectives.d.lastRed": "Most recent failure",
|
|
19539
19559
|
"perspectives.d.changedSince": "Changes since the last run",
|
|
19540
|
-
"perspectives.d.
|
|
19560
|
+
"perspectives.d.whyVerdict": "Why this verdict",
|
|
19541
19561
|
"perspectives.result.openRun": "Open this run in the hub",
|
|
19542
19562
|
"perspectives.result.ci": "CI",
|
|
19543
19563
|
"perspectives.rerun.state.needsRepair": "Needs repair",
|
|
19544
19564
|
"perspectives.rerun.state.rerunNeeded": "Re-run needed",
|
|
19545
|
-
"perspectives.rerun.state.unanswerable": "Can't tell",
|
|
19546
19565
|
"perspectives.rerun.state.inProgress": "In progress",
|
|
19547
19566
|
"perspectives.rerun.state.verified": "Verified",
|
|
19548
19567
|
"perspectives.rerun.vsDeploy": "judged against deploy",
|
|
@@ -19553,11 +19572,11 @@ const CLIENT_JS = `
|
|
|
19553
19572
|
"perspectives.rerun.touchedCount": "{n} deployed path(s) matched this case",
|
|
19554
19573
|
"perspectives.rerun.touchedUnknown": "a deploy since the last run matched this case",
|
|
19555
19574
|
"perspectives.rerun.inProgressHint": "an audit or a run is still going, or the audit has not caught up with the deploy",
|
|
19575
|
+
"perspectives.rerun.heldHint": "another job already holds this spec — acting on it now would race that job",
|
|
19556
19576
|
"perspectives.rerun.repair.testDrift": "the generated test no longer matches the code — re-record it",
|
|
19557
19577
|
"perspectives.rerun.repair.specChange": "the spec describes something the code no longer does — a human decides",
|
|
19558
19578
|
"perspectives.rerun.repair.auditUndecided": "the audit read the code and could not decide — a human looks",
|
|
19559
19579
|
"perspectives.rerun.repair.runFailed": "the last run failed — re-running it changes nothing until the cause is fixed",
|
|
19560
|
-
"perspectives.rerun.why.notEvaluated": "no run and no deploy has ever been recorded for this profile",
|
|
19561
19580
|
"perspectives.rerun.why.noSelectionInRange": "a deploy in range was recorded without a spec selection",
|
|
19562
19581
|
"perspectives.rerun.why.selectionUnknown": "the selector could not tell whether this case was affected",
|
|
19563
19582
|
"perspectives.rerun.why.noDeployLog": "no deploy log for this profile",
|
|
@@ -19566,7 +19585,6 @@ const CLIENT_JS = `
|
|
|
19566
19585
|
"perspectives.rerun.why.deployedShaNotInLog": "the last run's commit predates the retained deploy log",
|
|
19567
19586
|
"perspectives.rerun.why.gapInRange": "deploys are missing from the range",
|
|
19568
19587
|
"perspectives.rerun.why.unrecognized": "this hub reported a reason this UI does not recognise",
|
|
19569
|
-
"perspectives.rerun.fix.notEvaluated": "Nothing has been recorded for this profile at all. Wire ccqa hub deploy record into the deploy job and push a run report, so there is something to compare against.",
|
|
19570
19588
|
"perspectives.rerun.fix.noSelectionInRange": "A deploy in range was recorded without a spec selection, so nothing says whether it affected this case. Run ccqa select-specs in the deploy job and send its verdict with the deploy.",
|
|
19571
19589
|
"perspectives.rerun.fix.selectionUnknown": "A deploy in range was judged, but the selector could not decide this case. Re-run it to get a clean baseline.",
|
|
19572
19590
|
"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.",
|
|
@@ -19579,12 +19597,7 @@ const CLIENT_JS = `
|
|
|
19579
19597
|
"perspectives.rerun.loadFailed": "Loading re-run data failed",
|
|
19580
19598
|
"perspectives.rerun.noDeployLogBanner": "No deploy has been recorded for profile {profile}, so no case can be judged. Wire ccqa hub deploy record into the deploy job for this environment.",
|
|
19581
19599
|
"perspectives.rerun.deployHead": "deploy head",
|
|
19582
|
-
"perspectives.drift.state.notAudited": "Not audited",
|
|
19583
|
-
"perspectives.drift.state.clean": "No drift",
|
|
19584
|
-
"perspectives.drift.state.found": "Drift found", "perspectives.drift.state.unknown": "Can't tell",
|
|
19585
19600
|
"perspectives.drift.graded": "confirmed",
|
|
19586
|
-
"perspectives.drift.unsupported": "This hub does not report drift audit results. Upgrade the hub to enable it.",
|
|
19587
|
-
"perspectives.drift.loadFailed": "Loading drift data failed",
|
|
19588
19601
|
"prompt.card.record": "Recording browser actions",
|
|
19589
19602
|
"prompt.card.live": "Live run (AI-driven)",
|
|
19590
19603
|
"prompt.card.playwright": "Playwright test generation",
|
|
@@ -19676,15 +19689,14 @@ const CLIENT_JS = `
|
|
|
19676
19689
|
"perspectives.search": "ケースを検索…",
|
|
19677
19690
|
"perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
|
|
19678
19691
|
"perspectives.filter.live": "ライブ",
|
|
19679
|
-
"perspectives.filter.
|
|
19680
|
-
"perspectives.col.verdict": "
|
|
19681
|
-
"perspectives.col.run": "実行",
|
|
19692
|
+
"perspectives.filter.group.mode": "モード",
|
|
19693
|
+
"perspectives.col.verdict": "判定", "perspectives.col.audit": "監査",
|
|
19694
|
+
"perspectives.col.run": "実行",
|
|
19682
19695
|
"perspectives.audit.state.due": "監査待ち",
|
|
19683
|
-
"perspectives.audit.state.clean": "
|
|
19684
|
-
"perspectives.audit.state.drifted": "
|
|
19696
|
+
"perspectives.audit.state.clean": "ズレなし",
|
|
19697
|
+
"perspectives.audit.state.drifted": "ズレあり",
|
|
19685
19698
|
"perspectives.audit.state.undecided": "判定不能",
|
|
19686
|
-
"perspectives.
|
|
19687
|
-
"perspectives.run.state.needed": "要再実行", "perspectives.run.state.unknown": "判定できない",
|
|
19699
|
+
"perspectives.run.state.superseded": "実行待ち",
|
|
19688
19700
|
"perspectives.run.state.failed": "失敗", "perspectives.run.state.passed": "合格",
|
|
19689
19701
|
"perspectives.run.state.never": "未実行",
|
|
19690
19702
|
"perspectives.col.case": "ケース", "perspectives.col.mode": "モード",
|
|
@@ -19694,7 +19706,6 @@ const CLIENT_JS = `
|
|
|
19694
19706
|
"perspectives.loadFailed": "テスト観点の読み込みに失敗しました",
|
|
19695
19707
|
"perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
|
|
19696
19708
|
"perspectives.ov.cases": "ケース", "perspectives.ov.features": "機能",
|
|
19697
|
-
"perspectives.ov.axis.rerun": "実行", "perspectives.ov.axis.drift": "ドリフト",
|
|
19698
19709
|
"perspectives.d.preconditions": "前提条件", "perspectives.d.startScreen": "開始画面",
|
|
19699
19710
|
"perspectives.d.testCondition": "実行条件", "perspectives.d.spec": "spec",
|
|
19700
19711
|
"perspectives.note.label": "note",
|
|
@@ -19703,12 +19714,11 @@ const CLIENT_JS = `
|
|
|
19703
19714
|
"perspectives.note.error": "保存に失敗しました — 再試行してください",
|
|
19704
19715
|
"perspectives.d.lastRed": "直近の失敗",
|
|
19705
19716
|
"perspectives.d.changedSince": "前回実行以降の変更",
|
|
19706
|
-
"perspectives.d.
|
|
19717
|
+
"perspectives.d.whyVerdict": "この判定の理由",
|
|
19707
19718
|
"perspectives.result.openRun": "ハブでこの実行を開く",
|
|
19708
19719
|
"perspectives.result.ci": "CI",
|
|
19709
19720
|
"perspectives.rerun.state.needsRepair": "修正待ち",
|
|
19710
19721
|
"perspectives.rerun.state.rerunNeeded": "要再実行",
|
|
19711
|
-
"perspectives.rerun.state.unanswerable": "判定できない",
|
|
19712
19722
|
"perspectives.rerun.state.inProgress": "進行中",
|
|
19713
19723
|
"perspectives.rerun.state.verified": "検証済み",
|
|
19714
19724
|
"perspectives.rerun.vsDeploy": "判定基準: デプロイ",
|
|
@@ -19719,11 +19729,11 @@ const CLIENT_JS = `
|
|
|
19719
19729
|
"perspectives.rerun.touchedCount": "このケースに一致したデプロイ差分 {n} 件",
|
|
19720
19730
|
"perspectives.rerun.touchedUnknown": "前回実行以降のデプロイがこのケースに一致する変更を行っています",
|
|
19721
19731
|
"perspectives.rerun.inProgressHint": "監査か実行がまだ走っているか、監査がデプロイに追いついていません",
|
|
19732
|
+
"perspectives.rerun.heldHint": "このスペックは既に別のジョブが保持しています。今操作するとそのジョブと競合します",
|
|
19722
19733
|
"perspectives.rerun.repair.testDrift": "生成されたテストが古くなっています。録り直してください",
|
|
19723
19734
|
"perspectives.rerun.repair.specChange": "spec がコードのやめた動作を書いています。人が判断します",
|
|
19724
19735
|
"perspectives.rerun.repair.auditUndecided": "監査がコードを読んだうえで判定できませんでした。人が見ます",
|
|
19725
19736
|
"perspectives.rerun.repair.runFailed": "最後の実行が落ちています。原因を直すまで再実行しても変わりません",
|
|
19726
|
-
"perspectives.rerun.why.notEvaluated": "このプロファイルには実行もデプロイも記録がありません",
|
|
19727
19737
|
"perspectives.rerun.why.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあります",
|
|
19728
19738
|
"perspectives.rerun.why.selectionUnknown": "影響の有無を判定できませんでした",
|
|
19729
19739
|
"perspectives.rerun.why.noDeployLog": "このプロファイルのデプロイ記録がありません",
|
|
@@ -19732,7 +19742,6 @@ const CLIENT_JS = `
|
|
|
19732
19742
|
"perspectives.rerun.why.deployedShaNotInLog": "前回実行のcommitが保持中のデプロイログより古いです",
|
|
19733
19743
|
"perspectives.rerun.why.gapInRange": "対象範囲のデプロイ記録が欠けています",
|
|
19734
19744
|
"perspectives.rerun.why.unrecognized": "このUIが認識できない理由がハブから返されました",
|
|
19735
|
-
"perspectives.rerun.fix.notEvaluated": "このプロファイルには何も記録がありません。デプロイジョブに ccqa hub deploy record を組み込み、実行レポートを送ってください。比較する対象がそこで初めて生まれます。",
|
|
19736
19745
|
"perspectives.rerun.fix.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあり、このケースに影響したかどうかを示すものがありません。デプロイジョブで ccqa select-specs を実行し、判定をデプロイと一緒に送ってください。",
|
|
19737
19746
|
"perspectives.rerun.fix.selectionUnknown": "対象範囲のデプロイは判定されましたが、このケースについては判断がつきませんでした。再実行して基準を取り直してください。",
|
|
19738
19747
|
"perspectives.rerun.fix.noDeployLog": "このプロファイルのデプロイログに記録がありません。何がデプロイされたかをccqaに伝えるため、この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
@@ -19745,12 +19754,7 @@ const CLIENT_JS = `
|
|
|
19745
19754
|
"perspectives.rerun.loadFailed": "再実行の要否の読み込みに失敗しました",
|
|
19746
19755
|
"perspectives.rerun.noDeployLogBanner": "プロファイル {profile} にデプロイの記録がないため、どのケースも判定できません。この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
19747
19756
|
"perspectives.rerun.deployHead": "最新デプロイ",
|
|
19748
|
-
"perspectives.drift.state.notAudited": "未監査",
|
|
19749
|
-
"perspectives.drift.state.clean": "ズレなし",
|
|
19750
|
-
"perspectives.drift.state.found": "ズレあり", "perspectives.drift.state.unknown": "判定できない",
|
|
19751
19757
|
"perspectives.drift.graded": "人が確認",
|
|
19752
|
-
"perspectives.drift.unsupported": "このハブはdrift監査結果を返しません。利用するにはハブを更新してください。",
|
|
19753
|
-
"perspectives.drift.loadFailed": "drift監査結果の読み込みに失敗しました",
|
|
19754
19758
|
"prompt.card.record": "ブラウザ操作の記録",
|
|
19755
19759
|
"prompt.card.live": "ライブ実行(AI操作)",
|
|
19756
19760
|
"prompt.card.playwright": "Playwrightテスト生成",
|
|
@@ -21612,13 +21616,14 @@ const CLIENT_JS = `
|
|
|
21612
21616
|
|
|
21613
21617
|
// "rerun" is the RerunReport for the currently selected profile, or null when
|
|
21614
21618
|
// this hub can't answer (older hub, or the fetch failed) — in which case the
|
|
21615
|
-
//
|
|
21619
|
+
// three re-run columns are dropped rather than filled with blanks.
|
|
21616
21620
|
// "rerunSupported" is tri-state: null until the first answer, then whether
|
|
21617
21621
|
// this hub answers at all. Chip visibility follows it rather than the report,
|
|
21618
21622
|
// so switching profile doesn't drop the filter while the next one loads.
|
|
21619
21623
|
// "drift" is the DriftLedgerResponse, or null when unanswered (older hub, or
|
|
21620
21624
|
// a failed fetch) — not profile-scoped, so it does not reset when the
|
|
21621
|
-
// profile switcher changes (unlike "rerun" above).
|
|
21625
|
+
// profile switcher changes (unlike "rerun" above). Only feeds the audit
|
|
21626
|
+
// column's "audited at" line now; its own finding is superseded by rr.audit.
|
|
21622
21627
|
var perspState = {
|
|
21623
21628
|
doc: null, q: "", f: "all",
|
|
21624
21629
|
rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
|
|
@@ -21655,11 +21660,11 @@ const CLIENT_JS = `
|
|
|
21655
21660
|
"/rerun?profile=" + encodeURIComponent(state.profile);
|
|
21656
21661
|
}
|
|
21657
21662
|
|
|
21658
|
-
// Shared by rerun and drift, which differ only in path and i18n prefix.
|
|
21659
21663
|
// Resolves { report } or { note } and never rejects: a hub that predates
|
|
21660
|
-
// the endpoint costs only
|
|
21661
|
-
// only mean "no such route" — the endpoint's own 404 is "the
|
|
21662
|
-
// perspectives document", and this runs only after that
|
|
21664
|
+
// the endpoint costs only the columns it feeds, not the whole tab. A 404
|
|
21665
|
+
// here can only mean "no such route" — the endpoint's own 404 is "the
|
|
21666
|
+
// project has no perspectives document", and this runs only after that
|
|
21667
|
+
// document loaded.
|
|
21663
21668
|
function fetchLedgerColumn(path, i18nPrefix) {
|
|
21664
21669
|
return fetch(path, { headers: { Authorization: "Bearer " + state.token } }).then(function (res) {
|
|
21665
21670
|
if (res.status === 404) return { note: t(i18nPrefix + "unsupported"), kind: "info" };
|
|
@@ -21674,7 +21679,10 @@ const CLIENT_JS = `
|
|
|
21674
21679
|
|
|
21675
21680
|
// ── perspectives: drift ledger ────────────────────────────────────────
|
|
21676
21681
|
// Not profile-scoped (see perspState.drift above), so unlike rerunPath this
|
|
21677
|
-
// takes no ?profile=.
|
|
21682
|
+
// takes no ?profile=. Its own finding is superseded by the audit axis in
|
|
21683
|
+
// the /rerun report (ADR-0014); what survives into the view is only its
|
|
21684
|
+
// coordinate — when a spec was last audited — folded into the audit
|
|
21685
|
+
// column's evidence line (perspAuditCell).
|
|
21678
21686
|
|
|
21679
21687
|
function driftPath() {
|
|
21680
21688
|
return "/api/v1/projects/" + encodeURIComponent(state.project) + "/drift";
|
|
@@ -21726,16 +21734,25 @@ const CLIENT_JS = `
|
|
|
21726
21734
|
return text === prefix + reason ? t(prefix + "unrecognized") : text;
|
|
21727
21735
|
}
|
|
21728
21736
|
|
|
21729
|
-
//
|
|
21730
|
-
//
|
|
21731
|
-
//
|
|
21732
|
-
//
|
|
21733
|
-
//
|
|
21734
|
-
//
|
|
21735
|
-
|
|
21736
|
-
|
|
21737
|
+
// Every verdict that carries no evidence row explains itself here, in the
|
|
21738
|
+
// actionable phrasing the detail panel wants. decide() checks heldBy before
|
|
21739
|
+
// the audit axis, so a spec another job already holds must be explained by
|
|
21740
|
+
// that hold, not by the audit-hole annotation below — which only applies to
|
|
21741
|
+
// an "inProgress" verdict that decide() reached by falling through to a due
|
|
21742
|
+
// audit (ADR-0014). A verdict a newer hub invented falls through to the fix
|
|
21743
|
+
// lookup, so the row says the UI cannot read it rather than going blank —
|
|
21744
|
+
// which looks like missing data.
|
|
21745
|
+
function rerunWhyVerdict(rr) {
|
|
21737
21746
|
if (rr.verdict === "needsRepair") return rerunReasonText("perspectives.rerun.repair.", rerunRepairCause(rr));
|
|
21738
|
-
if (rr.verdict === "
|
|
21747
|
+
if (rr.verdict === "rerunNeeded" && rr.executionAssumedReached) {
|
|
21748
|
+
return rerunReasonText("perspectives.rerun.fix.", rr.executionAssumedReached);
|
|
21749
|
+
}
|
|
21750
|
+
if (rr.verdict === "inProgress") {
|
|
21751
|
+
if (rr.heldBy) return t("perspectives.rerun.heldHint");
|
|
21752
|
+
return rr.auditAssumedReached
|
|
21753
|
+
? rerunReasonText("perspectives.rerun.fix.", rr.auditAssumedReached)
|
|
21754
|
+
: t("perspectives.rerun.inProgressHint");
|
|
21755
|
+
}
|
|
21739
21756
|
return rerunReasonText("perspectives.rerun.fix.", rr.verdict);
|
|
21740
21757
|
}
|
|
21741
21758
|
|
|
@@ -21750,10 +21767,12 @@ const CLIENT_JS = `
|
|
|
21750
21767
|
|
|
21751
21768
|
// The short justification a table cell carries under its badge. Nothing here
|
|
21752
21769
|
// may collapse to a bare "up to date" — verified names the deploy it was
|
|
21753
|
-
// judged against, and
|
|
21770
|
+
// judged against, and a spec assumed reached names the hole that made it so
|
|
21771
|
+
// rather than claiming a deploy matched it.
|
|
21754
21772
|
function rerunCellWhy(rr) {
|
|
21755
21773
|
var head = perspState.rerun && perspState.rerun.deployHead;
|
|
21756
21774
|
if (rr.verdict === "rerunNeeded") {
|
|
21775
|
+
if (rr.executionAssumedReached) return rerunReasonText("perspectives.rerun.why.", rr.executionAssumedReached);
|
|
21757
21776
|
if (!rr.touchedBy || !rr.touchedBy.length) return t("perspectives.rerun.touchedUnknown");
|
|
21758
21777
|
return t("perspectives.rerun.touchedCount").replace("{n}", String(rr.touchedBy.length));
|
|
21759
21778
|
}
|
|
@@ -21761,8 +21780,7 @@ const CLIENT_JS = `
|
|
|
21761
21780
|
if (!head) return t("perspectives.rerun.noDeployHead");
|
|
21762
21781
|
return t("perspectives.rerun.vsDeploy") + " " + shortSha(head.sha) + " · " + relTime(head.at);
|
|
21763
21782
|
}
|
|
21764
|
-
|
|
21765
|
-
return rerunCannotJudge(rr);
|
|
21783
|
+
return rerunWhyVerdict(rr);
|
|
21766
21784
|
}
|
|
21767
21785
|
|
|
21768
21786
|
|
|
@@ -21775,27 +21793,26 @@ const CLIENT_JS = `
|
|
|
21775
21793
|
// Bar segments in drawing order: what a person must act on first, then what
|
|
21776
21794
|
// the pipeline still owes, then what needs nothing. "needsRepair" leads
|
|
21777
21795
|
// because it is the only verdict a run cannot clear — someone has to repair
|
|
21778
|
-
// the spec or the product.
|
|
21779
|
-
|
|
21780
|
-
// clear", which is what ADR-0010 forbids.
|
|
21781
|
-
var RERUN_ORDER = ["needsRepair", "rerunNeeded", "unanswerable", "inProgress", "verified"];
|
|
21796
|
+
// the spec or the product.
|
|
21797
|
+
var RERUN_ORDER = ["needsRepair", "rerunNeeded", "inProgress", "verified"];
|
|
21782
21798
|
var RERUN_SEG_CLASS = {
|
|
21783
|
-
needsRepair: "sg-needsrepair", rerunNeeded: "sg-rerunneeded",
|
|
21799
|
+
needsRepair: "sg-needsrepair", rerunNeeded: "sg-rerunneeded",
|
|
21784
21800
|
inProgress: "sg-inprogress", verified: "sg-verified"
|
|
21785
21801
|
};
|
|
21786
21802
|
|
|
21787
|
-
//
|
|
21788
|
-
//
|
|
21789
|
-
//
|
|
21790
|
-
//
|
|
21791
|
-
|
|
21803
|
+
// The one rule the summary bar and the verdict filter chips both answer
|
|
21804
|
+
// to: a case with no verdict, or a verdict a newer hub invented, counts as
|
|
21805
|
+
// needing a run — unanswered means unverified (ADR-0014). Both call this
|
|
21806
|
+
// rather than each keeping its own copy of the fallback.
|
|
21807
|
+
function rerunVerdictOf(rr) {
|
|
21808
|
+
var v = rr && rr.verdict;
|
|
21809
|
+
return v && RERUN_ORDER.indexOf(v) !== -1 ? v : "rerunNeeded";
|
|
21810
|
+
}
|
|
21811
|
+
|
|
21812
|
+
// One verdict per case, bucketed, via rerunVerdictOf above.
|
|
21792
21813
|
function rerunComposition(verdicts) {
|
|
21793
|
-
var counts = { needsRepair: 0, rerunNeeded: 0,
|
|
21794
|
-
verdicts.forEach(function (rr) {
|
|
21795
|
-
if (!rr || !rr.verdict) { counts.unanswerable += 1; return; }
|
|
21796
|
-
var known = Object.prototype.hasOwnProperty.call(counts, rr.verdict);
|
|
21797
|
-
counts[known ? rr.verdict : "unanswerable"] += 1;
|
|
21798
|
-
});
|
|
21814
|
+
var counts = { needsRepair: 0, rerunNeeded: 0, inProgress: 0, verified: 0 };
|
|
21815
|
+
verdicts.forEach(function (rr) { counts[rerunVerdictOf(rr)] += 1; });
|
|
21799
21816
|
return counts;
|
|
21800
21817
|
}
|
|
21801
21818
|
|
|
@@ -21811,52 +21828,69 @@ const CLIENT_JS = `
|
|
|
21811
21828
|
}
|
|
21812
21829
|
// --- end pure: rerun composition -----------------------------------------
|
|
21813
21830
|
|
|
21814
|
-
// --- pure:
|
|
21815
|
-
// Same shape as rerun composition above,
|
|
21816
|
-
// reason
|
|
21817
|
-
//
|
|
21818
|
-
//
|
|
21819
|
-
//
|
|
21820
|
-
|
|
21821
|
-
|
|
21822
|
-
|
|
21823
|
-
|
|
21824
|
-
|
|
21825
|
-
|
|
21826
|
-
//
|
|
21827
|
-
//
|
|
21828
|
-
|
|
21829
|
-
|
|
21830
|
-
|
|
21831
|
-
|
|
21831
|
+
// --- pure: execution composition ------------------------------------------
|
|
21832
|
+
// Same shape as rerun composition above, self-contained for the same
|
|
21833
|
+
// reason, and read from the same rr records: bucketed by the execution
|
|
21834
|
+
// axis rather than the derived verdict. The mapping duplicates
|
|
21835
|
+
// perspRunState's two renames (neverRun -> never, stale -> superseded)
|
|
21836
|
+
// rather than calling it, so this region stays independently liftable.
|
|
21837
|
+
var EXEC_ORDER = ["failed", "superseded", "never", "passed"];
|
|
21838
|
+
var EXEC_SEG_CLASS = {
|
|
21839
|
+
failed: "sg-exec-failed", superseded: "sg-exec-stale",
|
|
21840
|
+
never: "sg-exec-never", passed: "sg-exec-passed"
|
|
21841
|
+
};
|
|
21842
|
+
|
|
21843
|
+
// A case with no verdict at all reads as never run — same "unanswered
|
|
21844
|
+
// means unverified" rule rerunComposition applies, and the safe direction:
|
|
21845
|
+
// it never inflates "passed".
|
|
21846
|
+
function executionComposition(records) {
|
|
21847
|
+
var counts = { failed: 0, superseded: 0, never: 0, passed: 0 };
|
|
21848
|
+
records.forEach(function (rr) {
|
|
21849
|
+
var exec = rr && rr.execution;
|
|
21850
|
+
var key = !exec || exec === "neverRun" ? "never" : exec === "stale" ? "superseded" : exec;
|
|
21851
|
+
counts[Object.prototype.hasOwnProperty.call(counts, key) ? key : "never"] += 1;
|
|
21852
|
+
});
|
|
21853
|
+
return counts;
|
|
21832
21854
|
}
|
|
21833
21855
|
|
|
21834
|
-
//
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21856
|
+
// Only states with cases in them get drawn — see rerunSegments' comment above.
|
|
21857
|
+
function executionSegments(counts) {
|
|
21858
|
+
var out = [];
|
|
21859
|
+
EXEC_ORDER.forEach(function (key) {
|
|
21860
|
+
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls: EXEC_SEG_CLASS[key] });
|
|
21861
|
+
});
|
|
21862
|
+
return out;
|
|
21863
|
+
}
|
|
21864
|
+
// --- end pure: execution composition --------------------------------------
|
|
21865
|
+
|
|
21866
|
+
// --- pure: audit composition -----------------------------------------------
|
|
21867
|
+
// Same shape again, bucketed by the audit axis (ADR-0014).
|
|
21868
|
+
var AUDIT_ORDER = ["drifted", "undecided", "due", "clean"];
|
|
21869
|
+
var AUDIT_SEG_CLASS = {
|
|
21870
|
+
drifted: "sg-audit-drifted", undecided: "sg-audit-undecided",
|
|
21871
|
+
due: "sg-audit-due", clean: "sg-audit-clean"
|
|
21843
21872
|
};
|
|
21844
21873
|
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21874
|
+
// A case with no audit axis at all reads as due — the same "unanswered
|
|
21875
|
+
// means not yet cleared" rule as the other two axes.
|
|
21876
|
+
function auditComposition(records) {
|
|
21877
|
+
var counts = { drifted: 0, undecided: 0, due: 0, clean: 0 };
|
|
21878
|
+
records.forEach(function (rr) {
|
|
21879
|
+
var key = (rr && rr.audit) || "due";
|
|
21880
|
+
counts[Object.prototype.hasOwnProperty.call(counts, key) ? key : "due"] += 1;
|
|
21881
|
+
});
|
|
21848
21882
|
return counts;
|
|
21849
21883
|
}
|
|
21850
21884
|
|
|
21851
21885
|
// Only states with cases in them get drawn — see rerunSegments' comment above.
|
|
21852
|
-
function
|
|
21886
|
+
function auditSegments(counts) {
|
|
21853
21887
|
var out = [];
|
|
21854
|
-
|
|
21855
|
-
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls:
|
|
21888
|
+
AUDIT_ORDER.forEach(function (key) {
|
|
21889
|
+
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls: AUDIT_SEG_CLASS[key] });
|
|
21856
21890
|
});
|
|
21857
21891
|
return out;
|
|
21858
21892
|
}
|
|
21859
|
-
// --- end pure:
|
|
21893
|
+
// --- end pure: audit composition --------------------------------------------
|
|
21860
21894
|
|
|
21861
21895
|
// One ledger entry as "<short sha> · <when>", linking to the hub's run detail
|
|
21862
21896
|
// and, when that run recorded one, to the CI run. Clicks must not bubble: the
|
|
@@ -21914,30 +21948,37 @@ const CLIENT_JS = `
|
|
|
21914
21948
|
// still true" are two faces of one question, so the row answers it once and
|
|
21915
21949
|
// the detail panel keeps the coordinates.
|
|
21916
21950
|
//
|
|
21917
|
-
//
|
|
21918
|
-
//
|
|
21919
|
-
//
|
|
21920
|
-
|
|
21951
|
+
// --- pure: run-state labels ----------------------------------------------
|
|
21952
|
+
// Self-contained (no DOM, no closures) so rerun-view.test.ts can lift it and
|
|
21953
|
+
// check that every execution value the hub can send has a badge and wording.
|
|
21954
|
+
|
|
21955
|
+
// A recorded failure outranks the deploy that landed after it: a red result
|
|
21956
|
+
// is current information, and repeating it teaches nothing until someone
|
|
21957
|
+
// repairs it.
|
|
21921
21958
|
//
|
|
21922
|
-
//
|
|
21923
|
-
//
|
|
21924
|
-
//
|
|
21925
|
-
// it as passed or failed would claim a confidence nothing supports.
|
|
21959
|
+
// The wording keys are not the axis values: ADR-0010 reserves the freshness
|
|
21960
|
+
// adjectives for drift, so what the schema calls "stale" is shown as what it
|
|
21961
|
+
// means for this case — it has not run since the deploy.
|
|
21926
21962
|
function perspRunState(rr) {
|
|
21927
21963
|
if (!rr || !rr.execution) return null;
|
|
21928
|
-
|
|
21964
|
+
if (rr.execution === "neverRun") return "never";
|
|
21965
|
+
return rr.execution === "stale" ? "superseded" : rr.execution;
|
|
21929
21966
|
}
|
|
21930
21967
|
|
|
21931
21968
|
// Reuses the badge classes that already mean these things elsewhere rather
|
|
21932
|
-
// than minting a parallel palette: amber for "act on this",
|
|
21933
|
-
//
|
|
21934
|
-
var RUN_STATE_BADGE = { failed: "failed", passed: "passed", never: "rr-none" };
|
|
21969
|
+
// than minting a parallel palette: amber for "act on this", and the run
|
|
21970
|
+
// status colours for a result that still stands.
|
|
21971
|
+
var RUN_STATE_BADGE = { failed: "failed", passed: "passed", superseded: "rr-needed", never: "rr-none" };
|
|
21972
|
+
// --- end pure: run-state labels -------------------------------------------
|
|
21935
21973
|
|
|
21936
21974
|
// The primary column. The two axes beside it answer "why"; this one answers
|
|
21937
21975
|
// "who acts next", which is the only question a reader scanning a list of
|
|
21938
21976
|
// specs has. Exactly one value, needsRepair, asks for a person.
|
|
21977
|
+
// Same colours the summary bar uses, so a row and the bar above it cannot
|
|
21978
|
+
// disagree. needsRepair is the only verdict that asks for a person, so it
|
|
21979
|
+
// takes the attention colour and re-running — machine work — does not.
|
|
21939
21980
|
var VERDICT_BADGE = {
|
|
21940
|
-
needsRepair: "rr-
|
|
21981
|
+
needsRepair: "rr-repair", rerunNeeded: "rr-needed",
|
|
21941
21982
|
inProgress: "rr-none", verified: "passed"
|
|
21942
21983
|
};
|
|
21943
21984
|
|
|
@@ -21957,15 +21998,28 @@ const CLIENT_JS = `
|
|
|
21957
21998
|
return td;
|
|
21958
21999
|
}
|
|
21959
22000
|
|
|
21960
|
-
//
|
|
21961
|
-
//
|
|
21962
|
-
//
|
|
22001
|
+
// A reason that pinned a spec to a pending state without a demonstrable
|
|
22002
|
+
// deploy touch (ADR-0014, auditAssumedReached / executionAssumedReached).
|
|
22003
|
+
// The short form is always visible as a .cellsub; the title attribute
|
|
22004
|
+
// carries the longer, actionable form for hover, so what closes the hole
|
|
22005
|
+
// is available without depending on a pointer to find it.
|
|
22006
|
+
function assumedReachedSub(reason) {
|
|
22007
|
+
var sub = el("span", "cellsub", rerunReasonText("perspectives.rerun.why.", reason));
|
|
22008
|
+
sub.title = rerunReasonText("perspectives.rerun.fix.", reason);
|
|
22009
|
+
return sub;
|
|
22010
|
+
}
|
|
22011
|
+
|
|
22012
|
+
// Axis 1, as the hub derived it against the deployed commit — fresher than
|
|
22013
|
+
// the raw drift ledger, whose entry may predate the deploy. The ledger
|
|
22014
|
+
// survives here only as the "audited at" coordinate (ledgerLine below):
|
|
22015
|
+
// its own finding is superseded by rr.audit/rr.driftLabel above, so
|
|
22016
|
+
// showing both would repeat one answer in two vocabularies — the bug this
|
|
22017
|
+
// column used to have paired with a since-removed fourth column.
|
|
21963
22018
|
var AUDIT_BADGE = {
|
|
21964
|
-
due: "rr-none", clean: "passed", drifted: "failed",
|
|
21965
|
-
undecided: "rr-unknown", cannotTell: "rr-unknown"
|
|
22019
|
+
due: "rr-none", clean: "passed", drifted: "failed", undecided: "rr-unknown"
|
|
21966
22020
|
};
|
|
21967
22021
|
|
|
21968
|
-
function perspAuditCell(rr) {
|
|
22022
|
+
function perspAuditCell(rr, driftEntry) {
|
|
21969
22023
|
var td = el("td");
|
|
21970
22024
|
if (!rr || !rr.audit) {
|
|
21971
22025
|
td.appendChild(el("span", "muted", "\u2014"));
|
|
@@ -21977,6 +22031,23 @@ const CLIENT_JS = `
|
|
|
21977
22031
|
td.appendChild(badge);
|
|
21978
22032
|
if (rr.audit === "drifted" && rr.driftLabel) {
|
|
21979
22033
|
td.appendChild(el("span", "cellsub", labelText(rr.driftLabel)));
|
|
22034
|
+
} else if (rr.auditAssumedReached) {
|
|
22035
|
+
// Due because the log could not place the audit, not because a deploy
|
|
22036
|
+
// demonstrably reached it. Say which, or the whole column reads "due"
|
|
22037
|
+
// with no cause anywhere on the page.
|
|
22038
|
+
td.appendChild(assumedReachedSub(rr.auditAssumedReached));
|
|
22039
|
+
}
|
|
22040
|
+
// "audited at sha · when" — the same freshness evidence the execution
|
|
22041
|
+
// column gives its own last result, below. SpecRerun does not carry the
|
|
22042
|
+
// audit's own coordinate, so it comes from the drift ledger.
|
|
22043
|
+
if (driftEntry) {
|
|
22044
|
+
var sub = el("span", "cellsub");
|
|
22045
|
+
sub.appendChild(ledgerLine(driftEntry));
|
|
22046
|
+
if (driftEntry.graded) {
|
|
22047
|
+
sub.appendChild(document.createTextNode(" · "));
|
|
22048
|
+
sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
|
|
22049
|
+
}
|
|
22050
|
+
td.appendChild(sub);
|
|
21980
22051
|
}
|
|
21981
22052
|
return td;
|
|
21982
22053
|
}
|
|
@@ -21987,7 +22058,7 @@ const CLIENT_JS = `
|
|
|
21987
22058
|
// No verdict at all: this case is in the document but not in the report
|
|
21988
22059
|
// (added since it was computed). Not the same statement as "never run".
|
|
21989
22060
|
if (!runState) {
|
|
21990
|
-
td.appendChild(el("span", "muted", "
|
|
22061
|
+
td.appendChild(el("span", "muted", "\u2014"));
|
|
21991
22062
|
return td;
|
|
21992
22063
|
}
|
|
21993
22064
|
var badge = el("span", "badge " + RUN_STATE_BADGE[runState]);
|
|
@@ -21995,8 +22066,11 @@ const CLIENT_JS = `
|
|
|
21995
22066
|
badge.appendChild(document.createTextNode(" " + t("perspectives.run.state." + runState)));
|
|
21996
22067
|
td.appendChild(badge);
|
|
21997
22068
|
|
|
21998
|
-
// The sub-line is the coordinate of the result being reported
|
|
21999
|
-
//
|
|
22069
|
+
// The sub-line is the coordinate of the result being reported — the same
|
|
22070
|
+
// "when is this from" evidence the audit column carries above, so the two
|
|
22071
|
+
// axes read as siblings. Why the currency matters belongs to the verdict
|
|
22072
|
+
// column, except for the hole that pinned this to "pending" with no
|
|
22073
|
+
// deploy to point at.
|
|
22000
22074
|
if (runState !== "never") {
|
|
22001
22075
|
var last = lastResult(rr);
|
|
22002
22076
|
if (last) {
|
|
@@ -22005,53 +22079,7 @@ const CLIENT_JS = `
|
|
|
22005
22079
|
td.appendChild(sub);
|
|
22006
22080
|
}
|
|
22007
22081
|
}
|
|
22008
|
-
|
|
22009
|
-
}
|
|
22010
|
-
|
|
22011
|
-
// ── perspectives: drift ledger ────────────────────────────────────────
|
|
22012
|
-
// "Does this case still describe the product?" — a different question from
|
|
22013
|
-
// re-run above, and shown as its own column rather than folded into it.
|
|
22014
|
-
// Unlike re-run, drift carries no profile: an audit is a property of the
|
|
22015
|
-
// code at a commit, not of an environment, so this column never changes
|
|
22016
|
-
// when the profile switcher above it does.
|
|
22017
|
-
|
|
22018
|
-
var DRIFT_BADGE_CLASS = { notAudited: "dr-none", clean: "dr-clean", found: "dr-found", unknown: "dr-unknown" };
|
|
22019
|
-
|
|
22020
|
-
// driftState() lives with driftComposition/driftSegments above (the "pure:
|
|
22021
|
-
// drift composition" region) — the badge must not collapse "never audited"
|
|
22022
|
-
// and "audited, no drift found" into one grey, same reasoning either way.
|
|
22023
|
-
function driftBadge(state) {
|
|
22024
|
-
var span = el("span", "badge " + (DRIFT_BADGE_CLASS[state] || "dr-none"));
|
|
22025
|
-
span.appendChild(el("span", "d"));
|
|
22026
|
-
span.appendChild(document.createTextNode(" " + t("perspectives.drift.state." + state)));
|
|
22027
|
-
return span;
|
|
22028
|
-
}
|
|
22029
|
-
|
|
22030
|
-
function perspDriftCell(entry) {
|
|
22031
|
-
var td = el("td");
|
|
22032
|
-
td.appendChild(driftBadge(driftState(entry)));
|
|
22033
|
-
if (entry) {
|
|
22034
|
-
var sub = el("span", "cellsub");
|
|
22035
|
-
// The label and surface go here only when they add to the badge. A clean
|
|
22036
|
-
// audit has neither. An UNKNOWN one has both on paper, but the badge
|
|
22037
|
-
// already says "could not tell" — repeating it as a label says the same
|
|
22038
|
-
// thing in a second wording, and naming the surface it could not judge
|
|
22039
|
-
// claims more than the audit found.
|
|
22040
|
-
if (entry.label && driftState(entry) === "found") {
|
|
22041
|
-
sub.appendChild(document.createTextNode(
|
|
22042
|
-
labelText(entry.label) + (entry.surface ? " (" + t("diag.surface." + entry.surface) + ")" : "") + " · ",
|
|
22043
|
-
));
|
|
22044
|
-
}
|
|
22045
|
-
sub.appendChild(ledgerLine(entry));
|
|
22046
|
-
// A verdict someone has looked at is worth more than one nobody has, in
|
|
22047
|
-
// both directions: a confirmed finding is real, and a confirmed clean is
|
|
22048
|
-
// not just "the audit found nothing".
|
|
22049
|
-
if (entry.graded) {
|
|
22050
|
-
sub.appendChild(document.createTextNode(" · "));
|
|
22051
|
-
sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
|
|
22052
|
-
}
|
|
22053
|
-
td.appendChild(sub);
|
|
22054
|
-
}
|
|
22082
|
+
if (rr.executionAssumedReached) td.appendChild(assumedReachedSub(rr.executionAssumedReached));
|
|
22055
22083
|
return td;
|
|
22056
22084
|
}
|
|
22057
22085
|
|
|
@@ -22105,45 +22133,47 @@ const CLIENT_JS = `
|
|
|
22105
22133
|
return row;
|
|
22106
22134
|
}
|
|
22107
22135
|
|
|
22108
|
-
// Summary: the inventory as one line, then one row per axis —
|
|
22109
|
-
//
|
|
22110
|
-
//
|
|
22111
|
-
// are not lost; they moved onto the filter chips,
|
|
22112
|
-
// that filter would leave behind.
|
|
22136
|
+
// Summary: the inventory as one line, then one row per axis — verdict,
|
|
22137
|
+
// execution, audit, the same three groupings the table's columns show, so
|
|
22138
|
+
// the bars and the table never disagree about what a word means. The mode
|
|
22139
|
+
// and recorded-ness counts are not lost; they moved onto the filter chips,
|
|
22140
|
+
// where a count states what that filter would leave behind.
|
|
22113
22141
|
//
|
|
22114
22142
|
// With no re-run data (an older hub, a failed fetch, or a profile nothing
|
|
22115
|
-
// has been recorded on) every case is "not evaluated" and
|
|
22116
|
-
//
|
|
22117
|
-
// "nothing to do".
|
|
22118
|
-
// when it hasn't (older hub, failed fetch), its row is omitted entirely
|
|
22119
|
-
// rather than drawn as "all not audited" — that would read as a finding
|
|
22120
|
-
// instead of missing data.
|
|
22143
|
+
// has been recorded on) every case is "not evaluated" and all three bars
|
|
22144
|
+
// are omitted together, rather than showing a composition that reads as
|
|
22145
|
+
// "nothing to do".
|
|
22121
22146
|
function renderPerspOverview(doc) {
|
|
22122
22147
|
var host = document.getElementById("persp-ov");
|
|
22123
22148
|
clear(host);
|
|
22124
|
-
var
|
|
22125
|
-
var driftEntries = perspState.drift ? [] : null;
|
|
22149
|
+
var records = [];
|
|
22126
22150
|
doc.features.forEach(function (feature) {
|
|
22127
22151
|
feature.specs.forEach(function (spec) {
|
|
22128
|
-
|
|
22129
|
-
if (driftEntries) driftEntries.push(ledgerEntryFor(perspState.drift, feature, spec));
|
|
22152
|
+
records.push(ledgerEntryFor(perspState.rerun, feature, spec));
|
|
22130
22153
|
});
|
|
22131
22154
|
});
|
|
22132
22155
|
|
|
22133
22156
|
var inv = el("div", "ov-inv");
|
|
22134
|
-
inv.appendChild(el("b", null, String(
|
|
22157
|
+
inv.appendChild(el("b", null, String(records.length)));
|
|
22135
22158
|
inv.appendChild(document.createTextNode(" " + t("perspectives.ov.cases") + " / "));
|
|
22136
22159
|
inv.appendChild(el("b", null, String(doc.features.length)));
|
|
22137
22160
|
inv.appendChild(document.createTextNode(" " + t("perspectives.ov.features")));
|
|
22138
22161
|
host.appendChild(inv);
|
|
22139
|
-
if (!
|
|
22162
|
+
if (!records.length) return;
|
|
22140
22163
|
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22164
|
+
// Gated on the report as a whole: with none, none of the three axes has
|
|
22165
|
+
// anything to compose, and an un-composed bar would paint every case as
|
|
22166
|
+
// needing work — an overstatement about the hub rather than about the
|
|
22167
|
+
// specs.
|
|
22168
|
+
if (perspState.rerun) {
|
|
22145
22169
|
host.appendChild(ovAxisRow(
|
|
22146
|
-
t("perspectives.
|
|
22170
|
+
t("perspectives.col.verdict"), rerunSegments(rerunComposition(records)), "perspectives.rerun.state.", records.length,
|
|
22171
|
+
));
|
|
22172
|
+
host.appendChild(ovAxisRow(
|
|
22173
|
+
t("perspectives.col.run"), executionSegments(executionComposition(records)), "perspectives.run.state.", records.length,
|
|
22174
|
+
));
|
|
22175
|
+
host.appendChild(ovAxisRow(
|
|
22176
|
+
t("perspectives.col.audit"), auditSegments(auditComposition(records)), "perspectives.audit.state.", records.length,
|
|
22147
22177
|
));
|
|
22148
22178
|
}
|
|
22149
22179
|
}
|
|
@@ -22155,15 +22185,16 @@ const CLIENT_JS = `
|
|
|
22155
22185
|
function perspMatches(feature, spec, f) {
|
|
22156
22186
|
if (f === "deterministic" && perspMode(spec) !== "deterministic") return false;
|
|
22157
22187
|
if (f === "live" && perspMode(spec) !== "live") return false;
|
|
22158
|
-
|
|
22188
|
+
// The verdict chips (same words as RERUN_ORDER/the 判定 column) share
|
|
22189
|
+
// rerunVerdictOf with the summary bar, so an unrecognised verdict is
|
|
22190
|
+
// "rerunNeeded" in both places rather than matching no chip. With no
|
|
22191
|
+
// report at all there is nothing to filter on, so every chip yields
|
|
22192
|
+
// nothing.
|
|
22193
|
+
if (RERUN_ORDER.indexOf(f) !== -1) {
|
|
22194
|
+
if (!perspState.rerun) return false;
|
|
22159
22195
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22160
|
-
|
|
22161
|
-
// folding it in here would be exactly the overstatement ADR-0010 forbids.
|
|
22162
|
-
if (!rr || rr.verdict !== "rerunNeeded") return false;
|
|
22196
|
+
if (rerunVerdictOf(rr) !== f) return false;
|
|
22163
22197
|
}
|
|
22164
|
-
// Same asymmetry: an unaudited spec is not a quiet "probably clean", so it
|
|
22165
|
-
// does not belong under a chip that claims to list what drifted.
|
|
22166
|
-
if (f === "drift" && driftState(ledgerEntryFor(perspState.drift, feature, spec)) !== "found") return false;
|
|
22167
22198
|
if (perspState.q) {
|
|
22168
22199
|
var hay = (spec.title + " " + (spec.summary || "") + " " + spec.specName).toLowerCase();
|
|
22169
22200
|
if (hay.indexOf(perspState.q) === -1) return false;
|
|
@@ -22186,14 +22217,19 @@ const CLIENT_JS = `
|
|
|
22186
22217
|
// lift this region out of the rendered page and run it: which label the
|
|
22187
22218
|
// panel's evidence row wears, and whether it has a failure row at all.
|
|
22188
22219
|
|
|
22189
|
-
//
|
|
22190
|
-
//
|
|
22191
|
-
//
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
return
|
|
22195
|
-
|
|
22196
|
-
|
|
22220
|
+
// The deploy log answered for this case: the row can show what it holds.
|
|
22221
|
+
// A case the log could not place has no evidence to show even though its
|
|
22222
|
+
// verdict is the same — the assumption is the answer, not a finding.
|
|
22223
|
+
function rerunHasEvidence(rr) {
|
|
22224
|
+
if (rr.verdict === "verified") return true;
|
|
22225
|
+
return rr.verdict === "rerunNeeded" && !rr.executionAssumedReached;
|
|
22226
|
+
}
|
|
22227
|
+
|
|
22228
|
+
// Evidence is labelled by the timeframe it covers; everything else names why
|
|
22229
|
+
// the verdict landed — a different kind of content, and forcing one label
|
|
22230
|
+
// over both would make one of the two read as a lie.
|
|
22231
|
+
function rerunEvidenceLabelKey(rr) {
|
|
22232
|
+
return rerunHasEvidence(rr) ? "perspectives.d.changedSince" : "perspectives.d.whyVerdict";
|
|
22197
22233
|
}
|
|
22198
22234
|
|
|
22199
22235
|
// The failure row points at a run. With no failure there is nothing to point
|
|
@@ -22229,8 +22265,8 @@ const CLIENT_JS = `
|
|
|
22229
22265
|
// The label already states the timeframe, so the value never repeats it.
|
|
22230
22266
|
function rerunEvidenceValue(rr) {
|
|
22231
22267
|
var wrap = el("div");
|
|
22232
|
-
if (rr
|
|
22233
|
-
wrap.appendChild(el("div", "d-prose",
|
|
22268
|
+
if (!rerunHasEvidence(rr)) {
|
|
22269
|
+
wrap.appendChild(el("div", "d-prose", rerunWhyVerdict(rr)));
|
|
22234
22270
|
return wrap;
|
|
22235
22271
|
}
|
|
22236
22272
|
// Both states require a non-empty deploy log, so a head-less report
|
|
@@ -22280,7 +22316,7 @@ const CLIENT_JS = `
|
|
|
22280
22316
|
|
|
22281
22317
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22282
22318
|
if (rr) {
|
|
22283
|
-
row(rerunEvidenceLabelKey(rr
|
|
22319
|
+
row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
|
|
22284
22320
|
if (rerunHasFailure(rr)) row("perspectives.d.lastRed", ledgerLine(rr.lastRed));
|
|
22285
22321
|
}
|
|
22286
22322
|
frag.appendChild(dl);
|
|
@@ -22325,14 +22361,12 @@ const CLIENT_JS = `
|
|
|
22325
22361
|
var tbody = document.getElementById("persp-tbody");
|
|
22326
22362
|
clear(tbody);
|
|
22327
22363
|
// Hiding the <th>s (rather than emitting empty cells) leaves the table
|
|
22328
|
-
// exactly as it was on a hub that cannot answer the re-run
|
|
22364
|
+
// exactly as it was on a hub that cannot answer the re-run question.
|
|
22329
22365
|
var showRerun = perspState.rerun != null;
|
|
22330
|
-
var showDrift = perspState.drift != null;
|
|
22331
22366
|
document.getElementById("persp-th-verdict").hidden = !showRerun;
|
|
22332
22367
|
document.getElementById("persp-th-audit").hidden = !showRerun;
|
|
22333
22368
|
document.getElementById("persp-th-run").hidden = !showRerun;
|
|
22334
|
-
|
|
22335
|
-
var cols = 3 + (showRerun ? 3 : 0) + (showDrift ? 1 : 0);
|
|
22369
|
+
var cols = 3 + (showRerun ? 3 : 0);
|
|
22336
22370
|
var hits = 0;
|
|
22337
22371
|
doc.features.forEach(function (feature) {
|
|
22338
22372
|
var specs = feature.specs.filter(function (s) { return perspMatches(feature, s, perspState.f); });
|
|
@@ -22364,10 +22398,9 @@ const CLIENT_JS = `
|
|
|
22364
22398
|
if (showRerun) {
|
|
22365
22399
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22366
22400
|
row.appendChild(perspVerdictCell(rr));
|
|
22367
|
-
row.appendChild(perspAuditCell(rr));
|
|
22368
22401
|
row.appendChild(perspRunCell(rr));
|
|
22402
|
+
row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec)));
|
|
22369
22403
|
}
|
|
22370
|
-
if (showDrift) row.appendChild(perspDriftCell(ledgerEntryFor(perspState.drift, feature, spec)));
|
|
22371
22404
|
|
|
22372
22405
|
var chevTd = el("td", "c-chev");
|
|
22373
22406
|
chevTd.appendChild(el("span", "chev-i", "\\u25b6"));
|
|
@@ -22410,21 +22443,17 @@ const CLIENT_JS = `
|
|
|
22410
22443
|
renderPerspTable(doc);
|
|
22411
22444
|
}
|
|
22412
22445
|
|
|
22413
|
-
// The
|
|
22414
|
-
// otherwise
|
|
22415
|
-
// the active filter when the answer came back
|
|
22446
|
+
// The verdict chip group only exists while the hub answers the re-run
|
|
22447
|
+
// question; otherwise every one of them would filter everything away. Drop
|
|
22448
|
+
// back to "all" if one was the active filter when the answer came back
|
|
22449
|
+
// "not supported".
|
|
22416
22450
|
//
|
|
22417
22451
|
// Each chip also carries what it would yield — the mode breakdown the
|
|
22418
22452
|
// summary row used to spend four tiles on.
|
|
22419
22453
|
function syncPerspChips() {
|
|
22420
|
-
var
|
|
22421
|
-
|
|
22422
|
-
if (perspState.rerunSupported
|
|
22423
|
-
// Same rule as the drift column: no ledger, no chip. A chip reading "0"
|
|
22424
|
-
// would say "nothing drifted" when the hub was never asked.
|
|
22425
|
-
var driftChip = document.getElementById("persp-chip-drift");
|
|
22426
|
-
driftChip.hidden = perspState.drift == null;
|
|
22427
|
-
if (perspState.drift == null && perspState.f === "drift") perspState.f = "all";
|
|
22454
|
+
var group = document.getElementById("persp-verdict-chips");
|
|
22455
|
+
group.hidden = perspState.rerunSupported !== true;
|
|
22456
|
+
if (perspState.rerunSupported !== true && RERUN_ORDER.indexOf(perspState.f) !== -1) perspState.f = "all";
|
|
22428
22457
|
document.querySelectorAll("#view-perspectives .fchip").forEach(function (b) {
|
|
22429
22458
|
var f = b.getAttribute("data-f");
|
|
22430
22459
|
b.setAttribute("aria-pressed", String(f === perspState.f));
|
|
@@ -22432,7 +22461,8 @@ const CLIENT_JS = `
|
|
|
22432
22461
|
});
|
|
22433
22462
|
}
|
|
22434
22463
|
|
|
22435
|
-
//
|
|
22464
|
+
// boxId is a parameter rather than hardcoded so a future note box can reuse
|
|
22465
|
+
// this without copying it — today only "persp-rerun-note" calls it.
|
|
22436
22466
|
function setPerspNote(boxId, text, kind) {
|
|
22437
22467
|
var box = document.getElementById(boxId);
|
|
22438
22468
|
box.hidden = !text;
|
|
@@ -22468,7 +22498,6 @@ const CLIENT_JS = `
|
|
|
22468
22498
|
setPerspDeployHead(null);
|
|
22469
22499
|
perspState.rerun = null;
|
|
22470
22500
|
perspState.rerunSupported = null;
|
|
22471
|
-
setPerspNote("persp-drift-note", "");
|
|
22472
22501
|
perspState.drift = null;
|
|
22473
22502
|
syncPerspChips();
|
|
22474
22503
|
fetchPerspectives()
|
|
@@ -22491,9 +22520,9 @@ const CLIENT_JS = `
|
|
|
22491
22520
|
loadRerun().catch(function (err) {
|
|
22492
22521
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.loadFailed") + ": " + err.message, "warn");
|
|
22493
22522
|
}),
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
}),
|
|
22523
|
+
// Evidence-only (the "audited at" line in the audit column) — a
|
|
22524
|
+
// failed or unsupported fetch just omits that line, no banner.
|
|
22525
|
+
loadDrift().catch(function () {}),
|
|
22497
22526
|
]);
|
|
22498
22527
|
})
|
|
22499
22528
|
.catch(function (err) {
|
|
@@ -22506,6 +22535,82 @@ const CLIENT_JS = `
|
|
|
22506
22535
|
return state.project + "/" + state.profile;
|
|
22507
22536
|
}
|
|
22508
22537
|
|
|
22538
|
+
// --- pure: data-profile pick ---------------------------------------------
|
|
22539
|
+
// Self-contained on purpose (no DOM, no closures): the network probing this
|
|
22540
|
+
// feeds is what makes "has deploy data" answerable at all, but which
|
|
22541
|
+
// candidate wins from the results is a plain function worth pinning without
|
|
22542
|
+
// mocking a fetch.
|
|
22543
|
+
|
|
22544
|
+
// "default" is always offered (the API guarantees it) but usually holds no
|
|
22545
|
+
// deploys, so opening straight into it reads as broken: every row pending,
|
|
22546
|
+
// a "no deploy log" banner. Fallback for when no candidate's deploy log can
|
|
22547
|
+
// be confirmed (a fresh project): prefer a profile the run index shows has
|
|
22548
|
+
// runs, with exactly one such profile the only reasonable pick.
|
|
22549
|
+
function pickDataProfile(current, dataProfiles) {
|
|
22550
|
+
if (!dataProfiles.length || dataProfiles.indexOf(current) !== -1) return current;
|
|
22551
|
+
return dataProfiles.length === 1 ? dataProfiles[0] : dataProfiles.slice().sort()[0];
|
|
22552
|
+
}
|
|
22553
|
+
|
|
22554
|
+
// Every profile worth checking for deploy data, in preference order: the
|
|
22555
|
+
// current profile first (so an already-fine pick is left alone), then
|
|
22556
|
+
// run-index profiles (a profile with runs is a maintained environment),
|
|
22557
|
+
// then the project's full profile set. That last tier is what makes a
|
|
22558
|
+
// profile deploys are recorded under but nothing has ever run against
|
|
22559
|
+
// reachable at all — the run index alone has no way to see it.
|
|
22560
|
+
function dataProfileCandidates(current, dataProfiles, projectProfiles) {
|
|
22561
|
+
var seen = {};
|
|
22562
|
+
var out = [];
|
|
22563
|
+
[current].concat(dataProfiles, projectProfiles).forEach(function (p) {
|
|
22564
|
+
if (p && !seen[p]) { seen[p] = true; out.push(p); }
|
|
22565
|
+
});
|
|
22566
|
+
return out;
|
|
22567
|
+
}
|
|
22568
|
+
|
|
22569
|
+
// The deterministic half of resolveDataProfile below: given which
|
|
22570
|
+
// candidates actually have a deploy log (probed in parallel, so answers can
|
|
22571
|
+
// arrive in any order), pick the first by candidate order, not by response
|
|
22572
|
+
// order — the result must not depend on network timing. Falls back to
|
|
22573
|
+
// pickDataProfile's answer when nothing confirms (no project has deploy
|
|
22574
|
+
// data yet), leaving that case's behaviour unchanged.
|
|
22575
|
+
function pickFirstWithDeployLog(candidates, hasLog, current, dataProfiles) {
|
|
22576
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
22577
|
+
if (hasLog[i]) return candidates[i];
|
|
22578
|
+
}
|
|
22579
|
+
return pickDataProfile(current, dataProfiles);
|
|
22580
|
+
}
|
|
22581
|
+
// --- end pure: data-profile pick ------------------------------------------
|
|
22582
|
+
|
|
22583
|
+
// Confirms a candidate actually has a deploy log, rather than merely being
|
|
22584
|
+
// known to the run index or the secrets tab — the two facts pickDataProfile
|
|
22585
|
+
// used to conflate (a profile can hold runs, or secrets, with no deploy log
|
|
22586
|
+
// at all). limit=1 makes this a cheap existence check.
|
|
22587
|
+
function hasDeployLog(profile) {
|
|
22588
|
+
return apiFetch(
|
|
22589
|
+
"/api/v1/projects/" + encodeURIComponent(state.project) + "/deploys?profile=" + encodeURIComponent(profile) + "&limit=1",
|
|
22590
|
+
).then(function (data) { return !!(data && data.entries && data.entries.length); })
|
|
22591
|
+
.catch(function () { return false; });
|
|
22592
|
+
}
|
|
22593
|
+
|
|
22594
|
+
// The candidate set from /profiles is broader than the run index (see
|
|
22595
|
+
// dataProfileCandidates), so probe every candidate's deploy log in
|
|
22596
|
+
// parallel before letting pickFirstWithDeployLog decide.
|
|
22597
|
+
function resolveDataProfile(current, dataProfiles, projectProfiles) {
|
|
22598
|
+
var candidates = dataProfileCandidates(current, dataProfiles, projectProfiles);
|
|
22599
|
+
return Promise.all(candidates.map(hasDeployLog))
|
|
22600
|
+
.then(function (hasLog) { return pickFirstWithDeployLog(candidates, hasLog, current, dataProfiles); });
|
|
22601
|
+
}
|
|
22602
|
+
|
|
22603
|
+
// The full profile set (secrets tab's universe) is broader than the run
|
|
22604
|
+
// index: a profile deploys are recorded under but nothing has ever run
|
|
22605
|
+
// against is invisible to fetchRunIndex. Fetched independently from
|
|
22606
|
+
// loadProfiles, which mutates knownProfiles/state.profile as a side effect
|
|
22607
|
+
// the Secrets tab depends on and this must not trigger.
|
|
22608
|
+
function fetchProjectProfiles() {
|
|
22609
|
+
return apiFetch("/api/v1/projects/" + encodeURIComponent(state.project) + "/profiles")
|
|
22610
|
+
.then(function (data) { return (data && data.profiles) || []; })
|
|
22611
|
+
.catch(function () { return []; });
|
|
22612
|
+
}
|
|
22613
|
+
|
|
22509
22614
|
// Project-scoped (see fetchRunIndex) — never rejects, so this always
|
|
22510
22615
|
// re-renders once the run index settles, whichever of the three loads in
|
|
22511
22616
|
// loadPerspectives is slowest.
|
|
@@ -22513,7 +22618,22 @@ const CLIENT_JS = `
|
|
|
22513
22618
|
return fetchRunIndex().then(function (result) {
|
|
22514
22619
|
perspState.runUrls = result.urls;
|
|
22515
22620
|
perspState.rerunProfiles = result.profiles;
|
|
22516
|
-
|
|
22621
|
+
if (storedProfileForProject(state.project)) {
|
|
22622
|
+
renderPerspectives();
|
|
22623
|
+
return;
|
|
22624
|
+
}
|
|
22625
|
+
// Never overrides a choice the user made explicitly (the check above)
|
|
22626
|
+
// — this only fills in the very first, unopinionated default.
|
|
22627
|
+
return fetchProjectProfiles()
|
|
22628
|
+
.then(function (projectProfiles) { return resolveDataProfile(state.profile, result.profiles, projectProfiles); })
|
|
22629
|
+
.then(function (pick) {
|
|
22630
|
+
if (pick !== state.profile) {
|
|
22631
|
+
setProfile(pick);
|
|
22632
|
+
renderPerspectives();
|
|
22633
|
+
return reloadRerun();
|
|
22634
|
+
}
|
|
22635
|
+
renderPerspectives();
|
|
22636
|
+
});
|
|
22517
22637
|
});
|
|
22518
22638
|
}
|
|
22519
22639
|
|
|
@@ -22529,8 +22649,9 @@ const CLIENT_JS = `
|
|
|
22529
22649
|
if (rerun.note) {
|
|
22530
22650
|
setPerspNote("persp-rerun-note", rerun.note, rerun.kind);
|
|
22531
22651
|
} else if (perspState.rerun && !perspState.rerun.deployHead) {
|
|
22532
|
-
//
|
|
22533
|
-
// is missing and how to supply it,
|
|
22652
|
+
// With no deploy log nothing can be placed, so every case is assumed
|
|
22653
|
+
// reached. Say once, at the top, what is missing and how to supply it,
|
|
22654
|
+
// rather than repeating it on every row.
|
|
22534
22655
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.noDeployLogBanner").replace("{profile}", state.profile), "warn");
|
|
22535
22656
|
} else {
|
|
22536
22657
|
setPerspNote("persp-rerun-note", "");
|
|
@@ -22540,13 +22661,18 @@ const CLIENT_JS = `
|
|
|
22540
22661
|
}
|
|
22541
22662
|
|
|
22542
22663
|
// Loaded once per project open — never re-run on a profile switch, since
|
|
22543
|
-
// drift carries no profile (unlike loadRerun/reloadRerun below).
|
|
22664
|
+
// drift carries no profile (unlike loadRerun/reloadRerun below). Only the
|
|
22665
|
+
// ledger's own coordinate (when a spec was last audited) survives into the
|
|
22666
|
+
// view now — its finding is superseded by the fresher, deploy-aware audit
|
|
22667
|
+
// axis in the /rerun report — so there is nothing here worth a banner on
|
|
22668
|
+
// an older or unreachable hub.
|
|
22544
22669
|
function loadDrift() {
|
|
22545
|
-
return
|
|
22546
|
-
|
|
22547
|
-
|
|
22548
|
-
|
|
22549
|
-
|
|
22670
|
+
return fetch(driftPath(), { headers: { Authorization: "Bearer " + state.token } })
|
|
22671
|
+
.then(function (res) { return res.ok ? res.json() : null; }, function () { return null; })
|
|
22672
|
+
.then(function (report) {
|
|
22673
|
+
perspState.drift = report || null;
|
|
22674
|
+
renderPerspectives();
|
|
22675
|
+
});
|
|
22550
22676
|
}
|
|
22551
22677
|
|
|
22552
22678
|
// Switching profile re-asks only the profile-scoped question: the
|