ccqa 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/bin/ccqa.mjs +519 -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,24 @@ 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
|
-
green, dr-none (never audited) shares rr-none's grey. */
|
|
19349
|
-
.badge.rr-needed, .badge.dr-found { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }
|
|
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-unknown { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
|
|
19376
|
+
.badge.rr-unknown .d { background: var(--info); }
|
|
19377
|
+
.badge.rr-none { background: var(--surface-3); color: var(--muted); border-color: var(--border); }
|
|
19378
|
+
.badge.rr-none .d { background: var(--muted); }
|
|
19359
19379
|
.cellsub { display: block; margin-top: 3px; max-width: 260px; color: var(--muted); font-size: 11.5px; line-height: 1.45; }
|
|
19360
19380
|
.graded-mark { color: var(--fg-dim); font-weight: 600; }
|
|
19361
19381
|
.cellsub a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--border-strong); }
|
|
19362
19382
|
.cellsub a:hover { color: var(--fg); }
|
|
19363
19383
|
.persp-note { margin-bottom: 12px; }
|
|
19364
19384
|
.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. */
|
|
19385
|
+
/* The Perspectives toolbar carries search + two filter-chip groups + the
|
|
19386
|
+
profile selector, so it wraps instead of overflowing on a narrow window. */
|
|
19367
19387
|
#view-perspectives .toolbar { flex-wrap: wrap; }
|
|
19368
19388
|
.proj-menu.right { left: auto; right: 0; }
|
|
19369
19389
|
.d-note { margin-top: 12px; max-width: 900px; }
|
|
@@ -19510,15 +19530,14 @@ const CLIENT_JS = `
|
|
|
19510
19530
|
"perspectives.search": "Search cases…",
|
|
19511
19531
|
"perspectives.filter.all": "All", "perspectives.filter.deterministic": "Deterministic",
|
|
19512
19532
|
"perspectives.filter.live": "Live",
|
|
19513
|
-
"perspectives.filter.
|
|
19514
|
-
"perspectives.col.verdict": "
|
|
19515
|
-
"perspectives.col.run": "Execution",
|
|
19533
|
+
"perspectives.filter.group.mode": "Mode",
|
|
19534
|
+
"perspectives.col.verdict": "Verdict", "perspectives.col.audit": "Audit",
|
|
19535
|
+
"perspectives.col.run": "Execution",
|
|
19516
19536
|
"perspectives.audit.state.due": "Audit due",
|
|
19517
19537
|
"perspectives.audit.state.clean": "Describes the code",
|
|
19518
19538
|
"perspectives.audit.state.drifted": "Drifted",
|
|
19519
19539
|
"perspectives.audit.state.undecided": "Couldn't tell",
|
|
19520
|
-
"perspectives.
|
|
19521
|
-
"perspectives.run.state.needed": "re-run needed", "perspectives.run.state.unknown": "can't tell",
|
|
19540
|
+
"perspectives.run.state.superseded": "pending",
|
|
19522
19541
|
"perspectives.run.state.failed": "failed", "perspectives.run.state.passed": "passed",
|
|
19523
19542
|
"perspectives.run.state.never": "never run",
|
|
19524
19543
|
"perspectives.col.case": "Case", "perspectives.col.mode": "Mode",
|
|
@@ -19528,7 +19547,6 @@ const CLIENT_JS = `
|
|
|
19528
19547
|
"perspectives.loadFailed": "Loading perspectives failed",
|
|
19529
19548
|
"perspectives.mode.deterministic": "deterministic", "perspectives.mode.live": "live",
|
|
19530
19549
|
"perspectives.ov.cases": "cases", "perspectives.ov.features": "features",
|
|
19531
|
-
"perspectives.ov.axis.rerun": "Re-run", "perspectives.ov.axis.drift": "Drift",
|
|
19532
19550
|
"perspectives.d.preconditions": "Preconditions", "perspectives.d.startScreen": "Start screen",
|
|
19533
19551
|
"perspectives.d.testCondition": "Condition", "perspectives.d.spec": "spec",
|
|
19534
19552
|
"perspectives.note.label": "Note",
|
|
@@ -19537,12 +19555,11 @@ const CLIENT_JS = `
|
|
|
19537
19555
|
"perspectives.note.error": "Could not save — retry",
|
|
19538
19556
|
"perspectives.d.lastRed": "Most recent failure",
|
|
19539
19557
|
"perspectives.d.changedSince": "Changes since the last run",
|
|
19540
|
-
"perspectives.d.
|
|
19558
|
+
"perspectives.d.whyVerdict": "Why this verdict",
|
|
19541
19559
|
"perspectives.result.openRun": "Open this run in the hub",
|
|
19542
19560
|
"perspectives.result.ci": "CI",
|
|
19543
19561
|
"perspectives.rerun.state.needsRepair": "Needs repair",
|
|
19544
19562
|
"perspectives.rerun.state.rerunNeeded": "Re-run needed",
|
|
19545
|
-
"perspectives.rerun.state.unanswerable": "Can't tell",
|
|
19546
19563
|
"perspectives.rerun.state.inProgress": "In progress",
|
|
19547
19564
|
"perspectives.rerun.state.verified": "Verified",
|
|
19548
19565
|
"perspectives.rerun.vsDeploy": "judged against deploy",
|
|
@@ -19553,11 +19570,11 @@ const CLIENT_JS = `
|
|
|
19553
19570
|
"perspectives.rerun.touchedCount": "{n} deployed path(s) matched this case",
|
|
19554
19571
|
"perspectives.rerun.touchedUnknown": "a deploy since the last run matched this case",
|
|
19555
19572
|
"perspectives.rerun.inProgressHint": "an audit or a run is still going, or the audit has not caught up with the deploy",
|
|
19573
|
+
"perspectives.rerun.heldHint": "another job already holds this spec — acting on it now would race that job",
|
|
19556
19574
|
"perspectives.rerun.repair.testDrift": "the generated test no longer matches the code — re-record it",
|
|
19557
19575
|
"perspectives.rerun.repair.specChange": "the spec describes something the code no longer does — a human decides",
|
|
19558
19576
|
"perspectives.rerun.repair.auditUndecided": "the audit read the code and could not decide — a human looks",
|
|
19559
19577
|
"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
19578
|
"perspectives.rerun.why.noSelectionInRange": "a deploy in range was recorded without a spec selection",
|
|
19562
19579
|
"perspectives.rerun.why.selectionUnknown": "the selector could not tell whether this case was affected",
|
|
19563
19580
|
"perspectives.rerun.why.noDeployLog": "no deploy log for this profile",
|
|
@@ -19566,7 +19583,6 @@ const CLIENT_JS = `
|
|
|
19566
19583
|
"perspectives.rerun.why.deployedShaNotInLog": "the last run's commit predates the retained deploy log",
|
|
19567
19584
|
"perspectives.rerun.why.gapInRange": "deploys are missing from the range",
|
|
19568
19585
|
"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
19586
|
"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
19587
|
"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
19588
|
"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 +19595,7 @@ const CLIENT_JS = `
|
|
|
19579
19595
|
"perspectives.rerun.loadFailed": "Loading re-run data failed",
|
|
19580
19596
|
"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
19597
|
"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
19598
|
"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
19599
|
"prompt.card.record": "Recording browser actions",
|
|
19589
19600
|
"prompt.card.live": "Live run (AI-driven)",
|
|
19590
19601
|
"prompt.card.playwright": "Playwright test generation",
|
|
@@ -19676,15 +19687,14 @@ const CLIENT_JS = `
|
|
|
19676
19687
|
"perspectives.search": "ケースを検索…",
|
|
19677
19688
|
"perspectives.filter.all": "すべて", "perspectives.filter.deterministic": "決定的",
|
|
19678
19689
|
"perspectives.filter.live": "ライブ",
|
|
19679
|
-
"perspectives.filter.
|
|
19680
|
-
"perspectives.col.verdict": "
|
|
19681
|
-
"perspectives.col.run": "実行",
|
|
19690
|
+
"perspectives.filter.group.mode": "モード",
|
|
19691
|
+
"perspectives.col.verdict": "判定", "perspectives.col.audit": "監査",
|
|
19692
|
+
"perspectives.col.run": "実行",
|
|
19682
19693
|
"perspectives.audit.state.due": "監査待ち",
|
|
19683
|
-
"perspectives.audit.state.clean": "
|
|
19684
|
-
"perspectives.audit.state.drifted": "
|
|
19694
|
+
"perspectives.audit.state.clean": "ズレなし",
|
|
19695
|
+
"perspectives.audit.state.drifted": "ズレあり",
|
|
19685
19696
|
"perspectives.audit.state.undecided": "判定不能",
|
|
19686
|
-
"perspectives.
|
|
19687
|
-
"perspectives.run.state.needed": "要再実行", "perspectives.run.state.unknown": "判定できない",
|
|
19697
|
+
"perspectives.run.state.superseded": "実行待ち",
|
|
19688
19698
|
"perspectives.run.state.failed": "失敗", "perspectives.run.state.passed": "合格",
|
|
19689
19699
|
"perspectives.run.state.never": "未実行",
|
|
19690
19700
|
"perspectives.col.case": "ケース", "perspectives.col.mode": "モード",
|
|
@@ -19694,7 +19704,6 @@ const CLIENT_JS = `
|
|
|
19694
19704
|
"perspectives.loadFailed": "テスト観点の読み込みに失敗しました",
|
|
19695
19705
|
"perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
|
|
19696
19706
|
"perspectives.ov.cases": "ケース", "perspectives.ov.features": "機能",
|
|
19697
|
-
"perspectives.ov.axis.rerun": "実行", "perspectives.ov.axis.drift": "ドリフト",
|
|
19698
19707
|
"perspectives.d.preconditions": "前提条件", "perspectives.d.startScreen": "開始画面",
|
|
19699
19708
|
"perspectives.d.testCondition": "実行条件", "perspectives.d.spec": "spec",
|
|
19700
19709
|
"perspectives.note.label": "note",
|
|
@@ -19703,12 +19712,11 @@ const CLIENT_JS = `
|
|
|
19703
19712
|
"perspectives.note.error": "保存に失敗しました — 再試行してください",
|
|
19704
19713
|
"perspectives.d.lastRed": "直近の失敗",
|
|
19705
19714
|
"perspectives.d.changedSince": "前回実行以降の変更",
|
|
19706
|
-
"perspectives.d.
|
|
19715
|
+
"perspectives.d.whyVerdict": "この判定の理由",
|
|
19707
19716
|
"perspectives.result.openRun": "ハブでこの実行を開く",
|
|
19708
19717
|
"perspectives.result.ci": "CI",
|
|
19709
19718
|
"perspectives.rerun.state.needsRepair": "修正待ち",
|
|
19710
19719
|
"perspectives.rerun.state.rerunNeeded": "要再実行",
|
|
19711
|
-
"perspectives.rerun.state.unanswerable": "判定できない",
|
|
19712
19720
|
"perspectives.rerun.state.inProgress": "進行中",
|
|
19713
19721
|
"perspectives.rerun.state.verified": "検証済み",
|
|
19714
19722
|
"perspectives.rerun.vsDeploy": "判定基準: デプロイ",
|
|
@@ -19719,11 +19727,11 @@ const CLIENT_JS = `
|
|
|
19719
19727
|
"perspectives.rerun.touchedCount": "このケースに一致したデプロイ差分 {n} 件",
|
|
19720
19728
|
"perspectives.rerun.touchedUnknown": "前回実行以降のデプロイがこのケースに一致する変更を行っています",
|
|
19721
19729
|
"perspectives.rerun.inProgressHint": "監査か実行がまだ走っているか、監査がデプロイに追いついていません",
|
|
19730
|
+
"perspectives.rerun.heldHint": "このスペックは既に別のジョブが保持しています。今操作するとそのジョブと競合します",
|
|
19722
19731
|
"perspectives.rerun.repair.testDrift": "生成されたテストが古くなっています。録り直してください",
|
|
19723
19732
|
"perspectives.rerun.repair.specChange": "spec がコードのやめた動作を書いています。人が判断します",
|
|
19724
19733
|
"perspectives.rerun.repair.auditUndecided": "監査がコードを読んだうえで判定できませんでした。人が見ます",
|
|
19725
19734
|
"perspectives.rerun.repair.runFailed": "最後の実行が落ちています。原因を直すまで再実行しても変わりません",
|
|
19726
|
-
"perspectives.rerun.why.notEvaluated": "このプロファイルには実行もデプロイも記録がありません",
|
|
19727
19735
|
"perspectives.rerun.why.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあります",
|
|
19728
19736
|
"perspectives.rerun.why.selectionUnknown": "影響の有無を判定できませんでした",
|
|
19729
19737
|
"perspectives.rerun.why.noDeployLog": "このプロファイルのデプロイ記録がありません",
|
|
@@ -19732,7 +19740,6 @@ const CLIENT_JS = `
|
|
|
19732
19740
|
"perspectives.rerun.why.deployedShaNotInLog": "前回実行のcommitが保持中のデプロイログより古いです",
|
|
19733
19741
|
"perspectives.rerun.why.gapInRange": "対象範囲のデプロイ記録が欠けています",
|
|
19734
19742
|
"perspectives.rerun.why.unrecognized": "このUIが認識できない理由がハブから返されました",
|
|
19735
|
-
"perspectives.rerun.fix.notEvaluated": "このプロファイルには何も記録がありません。デプロイジョブに ccqa hub deploy record を組み込み、実行レポートを送ってください。比較する対象がそこで初めて生まれます。",
|
|
19736
19743
|
"perspectives.rerun.fix.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあり、このケースに影響したかどうかを示すものがありません。デプロイジョブで ccqa select-specs を実行し、判定をデプロイと一緒に送ってください。",
|
|
19737
19744
|
"perspectives.rerun.fix.selectionUnknown": "対象範囲のデプロイは判定されましたが、このケースについては判断がつきませんでした。再実行して基準を取り直してください。",
|
|
19738
19745
|
"perspectives.rerun.fix.noDeployLog": "このプロファイルのデプロイログに記録がありません。何がデプロイされたかをccqaに伝えるため、この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
@@ -19745,12 +19752,7 @@ const CLIENT_JS = `
|
|
|
19745
19752
|
"perspectives.rerun.loadFailed": "再実行の要否の読み込みに失敗しました",
|
|
19746
19753
|
"perspectives.rerun.noDeployLogBanner": "プロファイル {profile} にデプロイの記録がないため、どのケースも判定できません。この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
19747
19754
|
"perspectives.rerun.deployHead": "最新デプロイ",
|
|
19748
|
-
"perspectives.drift.state.notAudited": "未監査",
|
|
19749
|
-
"perspectives.drift.state.clean": "ズレなし",
|
|
19750
|
-
"perspectives.drift.state.found": "ズレあり", "perspectives.drift.state.unknown": "判定できない",
|
|
19751
19755
|
"perspectives.drift.graded": "人が確認",
|
|
19752
|
-
"perspectives.drift.unsupported": "このハブはdrift監査結果を返しません。利用するにはハブを更新してください。",
|
|
19753
|
-
"perspectives.drift.loadFailed": "drift監査結果の読み込みに失敗しました",
|
|
19754
19756
|
"prompt.card.record": "ブラウザ操作の記録",
|
|
19755
19757
|
"prompt.card.live": "ライブ実行(AI操作)",
|
|
19756
19758
|
"prompt.card.playwright": "Playwrightテスト生成",
|
|
@@ -21612,13 +21614,14 @@ const CLIENT_JS = `
|
|
|
21612
21614
|
|
|
21613
21615
|
// "rerun" is the RerunReport for the currently selected profile, or null when
|
|
21614
21616
|
// this hub can't answer (older hub, or the fetch failed) — in which case the
|
|
21615
|
-
//
|
|
21617
|
+
// three re-run columns are dropped rather than filled with blanks.
|
|
21616
21618
|
// "rerunSupported" is tri-state: null until the first answer, then whether
|
|
21617
21619
|
// this hub answers at all. Chip visibility follows it rather than the report,
|
|
21618
21620
|
// so switching profile doesn't drop the filter while the next one loads.
|
|
21619
21621
|
// "drift" is the DriftLedgerResponse, or null when unanswered (older hub, or
|
|
21620
21622
|
// a failed fetch) — not profile-scoped, so it does not reset when the
|
|
21621
|
-
// profile switcher changes (unlike "rerun" above).
|
|
21623
|
+
// profile switcher changes (unlike "rerun" above). Only feeds the audit
|
|
21624
|
+
// column's "audited at" line now; its own finding is superseded by rr.audit.
|
|
21622
21625
|
var perspState = {
|
|
21623
21626
|
doc: null, q: "", f: "all",
|
|
21624
21627
|
rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
|
|
@@ -21655,11 +21658,11 @@ const CLIENT_JS = `
|
|
|
21655
21658
|
"/rerun?profile=" + encodeURIComponent(state.profile);
|
|
21656
21659
|
}
|
|
21657
21660
|
|
|
21658
|
-
// Shared by rerun and drift, which differ only in path and i18n prefix.
|
|
21659
21661
|
// 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
|
|
21662
|
+
// the endpoint costs only the columns it feeds, not the whole tab. A 404
|
|
21663
|
+
// here can only mean "no such route" — the endpoint's own 404 is "the
|
|
21664
|
+
// project has no perspectives document", and this runs only after that
|
|
21665
|
+
// document loaded.
|
|
21663
21666
|
function fetchLedgerColumn(path, i18nPrefix) {
|
|
21664
21667
|
return fetch(path, { headers: { Authorization: "Bearer " + state.token } }).then(function (res) {
|
|
21665
21668
|
if (res.status === 404) return { note: t(i18nPrefix + "unsupported"), kind: "info" };
|
|
@@ -21674,7 +21677,10 @@ const CLIENT_JS = `
|
|
|
21674
21677
|
|
|
21675
21678
|
// ── perspectives: drift ledger ────────────────────────────────────────
|
|
21676
21679
|
// Not profile-scoped (see perspState.drift above), so unlike rerunPath this
|
|
21677
|
-
// takes no ?profile=.
|
|
21680
|
+
// takes no ?profile=. Its own finding is superseded by the audit axis in
|
|
21681
|
+
// the /rerun report (ADR-0014); what survives into the view is only its
|
|
21682
|
+
// coordinate — when a spec was last audited — folded into the audit
|
|
21683
|
+
// column's evidence line (perspAuditCell).
|
|
21678
21684
|
|
|
21679
21685
|
function driftPath() {
|
|
21680
21686
|
return "/api/v1/projects/" + encodeURIComponent(state.project) + "/drift";
|
|
@@ -21726,16 +21732,25 @@ const CLIENT_JS = `
|
|
|
21726
21732
|
return text === prefix + reason ? t(prefix + "unrecognized") : text;
|
|
21727
21733
|
}
|
|
21728
21734
|
|
|
21729
|
-
//
|
|
21730
|
-
//
|
|
21731
|
-
//
|
|
21732
|
-
//
|
|
21733
|
-
//
|
|
21734
|
-
//
|
|
21735
|
-
|
|
21736
|
-
|
|
21735
|
+
// Every verdict that carries no evidence row explains itself here, in the
|
|
21736
|
+
// actionable phrasing the detail panel wants. decide() checks heldBy before
|
|
21737
|
+
// the audit axis, so a spec another job already holds must be explained by
|
|
21738
|
+
// that hold, not by the audit-hole annotation below — which only applies to
|
|
21739
|
+
// an "inProgress" verdict that decide() reached by falling through to a due
|
|
21740
|
+
// audit (ADR-0014). A verdict a newer hub invented falls through to the fix
|
|
21741
|
+
// lookup, so the row says the UI cannot read it rather than going blank —
|
|
21742
|
+
// which looks like missing data.
|
|
21743
|
+
function rerunWhyVerdict(rr) {
|
|
21737
21744
|
if (rr.verdict === "needsRepair") return rerunReasonText("perspectives.rerun.repair.", rerunRepairCause(rr));
|
|
21738
|
-
if (rr.verdict === "
|
|
21745
|
+
if (rr.verdict === "rerunNeeded" && rr.executionAssumedReached) {
|
|
21746
|
+
return rerunReasonText("perspectives.rerun.fix.", rr.executionAssumedReached);
|
|
21747
|
+
}
|
|
21748
|
+
if (rr.verdict === "inProgress") {
|
|
21749
|
+
if (rr.heldBy) return t("perspectives.rerun.heldHint");
|
|
21750
|
+
return rr.auditAssumedReached
|
|
21751
|
+
? rerunReasonText("perspectives.rerun.fix.", rr.auditAssumedReached)
|
|
21752
|
+
: t("perspectives.rerun.inProgressHint");
|
|
21753
|
+
}
|
|
21739
21754
|
return rerunReasonText("perspectives.rerun.fix.", rr.verdict);
|
|
21740
21755
|
}
|
|
21741
21756
|
|
|
@@ -21750,10 +21765,12 @@ const CLIENT_JS = `
|
|
|
21750
21765
|
|
|
21751
21766
|
// The short justification a table cell carries under its badge. Nothing here
|
|
21752
21767
|
// may collapse to a bare "up to date" — verified names the deploy it was
|
|
21753
|
-
// judged against, and
|
|
21768
|
+
// judged against, and a spec assumed reached names the hole that made it so
|
|
21769
|
+
// rather than claiming a deploy matched it.
|
|
21754
21770
|
function rerunCellWhy(rr) {
|
|
21755
21771
|
var head = perspState.rerun && perspState.rerun.deployHead;
|
|
21756
21772
|
if (rr.verdict === "rerunNeeded") {
|
|
21773
|
+
if (rr.executionAssumedReached) return rerunReasonText("perspectives.rerun.why.", rr.executionAssumedReached);
|
|
21757
21774
|
if (!rr.touchedBy || !rr.touchedBy.length) return t("perspectives.rerun.touchedUnknown");
|
|
21758
21775
|
return t("perspectives.rerun.touchedCount").replace("{n}", String(rr.touchedBy.length));
|
|
21759
21776
|
}
|
|
@@ -21761,8 +21778,7 @@ const CLIENT_JS = `
|
|
|
21761
21778
|
if (!head) return t("perspectives.rerun.noDeployHead");
|
|
21762
21779
|
return t("perspectives.rerun.vsDeploy") + " " + shortSha(head.sha) + " · " + relTime(head.at);
|
|
21763
21780
|
}
|
|
21764
|
-
|
|
21765
|
-
return rerunCannotJudge(rr);
|
|
21781
|
+
return rerunWhyVerdict(rr);
|
|
21766
21782
|
}
|
|
21767
21783
|
|
|
21768
21784
|
|
|
@@ -21775,27 +21791,26 @@ const CLIENT_JS = `
|
|
|
21775
21791
|
// Bar segments in drawing order: what a person must act on first, then what
|
|
21776
21792
|
// the pipeline still owes, then what needs nothing. "needsRepair" leads
|
|
21777
21793
|
// 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"];
|
|
21794
|
+
// the spec or the product.
|
|
21795
|
+
var RERUN_ORDER = ["needsRepair", "rerunNeeded", "inProgress", "verified"];
|
|
21782
21796
|
var RERUN_SEG_CLASS = {
|
|
21783
|
-
needsRepair: "sg-needsrepair", rerunNeeded: "sg-rerunneeded",
|
|
21797
|
+
needsRepair: "sg-needsrepair", rerunNeeded: "sg-rerunneeded",
|
|
21784
21798
|
inProgress: "sg-inprogress", verified: "sg-verified"
|
|
21785
21799
|
};
|
|
21786
21800
|
|
|
21787
|
-
//
|
|
21788
|
-
//
|
|
21789
|
-
//
|
|
21790
|
-
//
|
|
21791
|
-
|
|
21801
|
+
// The one rule the summary bar and the verdict filter chips both answer
|
|
21802
|
+
// to: a case with no verdict, or a verdict a newer hub invented, counts as
|
|
21803
|
+
// needing a run — unanswered means unverified (ADR-0014). Both call this
|
|
21804
|
+
// rather than each keeping its own copy of the fallback.
|
|
21805
|
+
function rerunVerdictOf(rr) {
|
|
21806
|
+
var v = rr && rr.verdict;
|
|
21807
|
+
return v && RERUN_ORDER.indexOf(v) !== -1 ? v : "rerunNeeded";
|
|
21808
|
+
}
|
|
21809
|
+
|
|
21810
|
+
// One verdict per case, bucketed, via rerunVerdictOf above.
|
|
21792
21811
|
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
|
-
});
|
|
21812
|
+
var counts = { needsRepair: 0, rerunNeeded: 0, inProgress: 0, verified: 0 };
|
|
21813
|
+
verdicts.forEach(function (rr) { counts[rerunVerdictOf(rr)] += 1; });
|
|
21799
21814
|
return counts;
|
|
21800
21815
|
}
|
|
21801
21816
|
|
|
@@ -21811,52 +21826,69 @@ const CLIENT_JS = `
|
|
|
21811
21826
|
}
|
|
21812
21827
|
// --- end pure: rerun composition -----------------------------------------
|
|
21813
21828
|
|
|
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
|
-
|
|
21829
|
+
// --- pure: execution composition ------------------------------------------
|
|
21830
|
+
// Same shape as rerun composition above, self-contained for the same
|
|
21831
|
+
// reason, and read from the same rr records: bucketed by the execution
|
|
21832
|
+
// axis rather than the derived verdict. The mapping duplicates
|
|
21833
|
+
// perspRunState's two renames (neverRun -> never, stale -> superseded)
|
|
21834
|
+
// rather than calling it, so this region stays independently liftable.
|
|
21835
|
+
var EXEC_ORDER = ["failed", "superseded", "never", "passed"];
|
|
21836
|
+
var EXEC_SEG_CLASS = {
|
|
21837
|
+
failed: "sg-exec-failed", superseded: "sg-exec-stale",
|
|
21838
|
+
never: "sg-exec-never", passed: "sg-exec-passed"
|
|
21839
|
+
};
|
|
21840
|
+
|
|
21841
|
+
// A case with no verdict at all reads as never run — same "unanswered
|
|
21842
|
+
// means unverified" rule rerunComposition applies, and the safe direction:
|
|
21843
|
+
// it never inflates "passed".
|
|
21844
|
+
function executionComposition(records) {
|
|
21845
|
+
var counts = { failed: 0, superseded: 0, never: 0, passed: 0 };
|
|
21846
|
+
records.forEach(function (rr) {
|
|
21847
|
+
var exec = rr && rr.execution;
|
|
21848
|
+
var key = !exec || exec === "neverRun" ? "never" : exec === "stale" ? "superseded" : exec;
|
|
21849
|
+
counts[Object.prototype.hasOwnProperty.call(counts, key) ? key : "never"] += 1;
|
|
21850
|
+
});
|
|
21851
|
+
return counts;
|
|
21832
21852
|
}
|
|
21833
21853
|
|
|
21834
|
-
//
|
|
21835
|
-
|
|
21836
|
-
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21854
|
+
// Only states with cases in them get drawn — see rerunSegments' comment above.
|
|
21855
|
+
function executionSegments(counts) {
|
|
21856
|
+
var out = [];
|
|
21857
|
+
EXEC_ORDER.forEach(function (key) {
|
|
21858
|
+
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls: EXEC_SEG_CLASS[key] });
|
|
21859
|
+
});
|
|
21860
|
+
return out;
|
|
21861
|
+
}
|
|
21862
|
+
// --- end pure: execution composition --------------------------------------
|
|
21863
|
+
|
|
21864
|
+
// --- pure: audit composition -----------------------------------------------
|
|
21865
|
+
// Same shape again, bucketed by the audit axis (ADR-0014).
|
|
21866
|
+
var AUDIT_ORDER = ["drifted", "undecided", "due", "clean"];
|
|
21867
|
+
var AUDIT_SEG_CLASS = {
|
|
21868
|
+
drifted: "sg-audit-drifted", undecided: "sg-audit-undecided",
|
|
21869
|
+
due: "sg-audit-due", clean: "sg-audit-clean"
|
|
21843
21870
|
};
|
|
21844
21871
|
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21872
|
+
// A case with no audit axis at all reads as due — the same "unanswered
|
|
21873
|
+
// means not yet cleared" rule as the other two axes.
|
|
21874
|
+
function auditComposition(records) {
|
|
21875
|
+
var counts = { drifted: 0, undecided: 0, due: 0, clean: 0 };
|
|
21876
|
+
records.forEach(function (rr) {
|
|
21877
|
+
var key = (rr && rr.audit) || "due";
|
|
21878
|
+
counts[Object.prototype.hasOwnProperty.call(counts, key) ? key : "due"] += 1;
|
|
21879
|
+
});
|
|
21848
21880
|
return counts;
|
|
21849
21881
|
}
|
|
21850
21882
|
|
|
21851
21883
|
// Only states with cases in them get drawn — see rerunSegments' comment above.
|
|
21852
|
-
function
|
|
21884
|
+
function auditSegments(counts) {
|
|
21853
21885
|
var out = [];
|
|
21854
|
-
|
|
21855
|
-
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls:
|
|
21886
|
+
AUDIT_ORDER.forEach(function (key) {
|
|
21887
|
+
if (counts[key] > 0) out.push({ state: key, count: counts[key], cls: AUDIT_SEG_CLASS[key] });
|
|
21856
21888
|
});
|
|
21857
21889
|
return out;
|
|
21858
21890
|
}
|
|
21859
|
-
// --- end pure:
|
|
21891
|
+
// --- end pure: audit composition --------------------------------------------
|
|
21860
21892
|
|
|
21861
21893
|
// One ledger entry as "<short sha> · <when>", linking to the hub's run detail
|
|
21862
21894
|
// and, when that run recorded one, to the CI run. Clicks must not bubble: the
|
|
@@ -21914,30 +21946,34 @@ const CLIENT_JS = `
|
|
|
21914
21946
|
// still true" are two faces of one question, so the row answers it once and
|
|
21915
21947
|
// the detail panel keeps the coordinates.
|
|
21916
21948
|
//
|
|
21917
|
-
//
|
|
21918
|
-
//
|
|
21919
|
-
//
|
|
21920
|
-
|
|
21949
|
+
// --- pure: run-state labels ----------------------------------------------
|
|
21950
|
+
// Self-contained (no DOM, no closures) so rerun-view.test.ts can lift it and
|
|
21951
|
+
// check that every execution value the hub can send has a badge and wording.
|
|
21952
|
+
|
|
21953
|
+
// A recorded failure outranks the deploy that landed after it: a red result
|
|
21954
|
+
// is current information, and repeating it teaches nothing until someone
|
|
21955
|
+
// repairs it.
|
|
21921
21956
|
//
|
|
21922
|
-
//
|
|
21923
|
-
//
|
|
21924
|
-
//
|
|
21925
|
-
// it as passed or failed would claim a confidence nothing supports.
|
|
21957
|
+
// The wording keys are not the axis values: ADR-0010 reserves the freshness
|
|
21958
|
+
// adjectives for drift, so what the schema calls "stale" is shown as what it
|
|
21959
|
+
// means for this case — it has not run since the deploy.
|
|
21926
21960
|
function perspRunState(rr) {
|
|
21927
21961
|
if (!rr || !rr.execution) return null;
|
|
21928
|
-
|
|
21962
|
+
if (rr.execution === "neverRun") return "never";
|
|
21963
|
+
return rr.execution === "stale" ? "superseded" : rr.execution;
|
|
21929
21964
|
}
|
|
21930
21965
|
|
|
21931
21966
|
// 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" };
|
|
21967
|
+
// than minting a parallel palette: amber for "act on this", and the run
|
|
21968
|
+
// status colours for a result that still stands.
|
|
21969
|
+
var RUN_STATE_BADGE = { failed: "failed", passed: "passed", superseded: "rr-needed", never: "rr-none" };
|
|
21970
|
+
// --- end pure: run-state labels -------------------------------------------
|
|
21935
21971
|
|
|
21936
21972
|
// The primary column. The two axes beside it answer "why"; this one answers
|
|
21937
21973
|
// "who acts next", which is the only question a reader scanning a list of
|
|
21938
21974
|
// specs has. Exactly one value, needsRepair, asks for a person.
|
|
21939
21975
|
var VERDICT_BADGE = {
|
|
21940
|
-
needsRepair: "rr-needed", rerunNeeded: "rr-needed",
|
|
21976
|
+
needsRepair: "rr-needed", rerunNeeded: "rr-needed",
|
|
21941
21977
|
inProgress: "rr-none", verified: "passed"
|
|
21942
21978
|
};
|
|
21943
21979
|
|
|
@@ -21957,15 +21993,28 @@ const CLIENT_JS = `
|
|
|
21957
21993
|
return td;
|
|
21958
21994
|
}
|
|
21959
21995
|
|
|
21960
|
-
//
|
|
21961
|
-
//
|
|
21962
|
-
//
|
|
21996
|
+
// A reason that pinned a spec to a pending state without a demonstrable
|
|
21997
|
+
// deploy touch (ADR-0014, auditAssumedReached / executionAssumedReached).
|
|
21998
|
+
// The short form is always visible as a .cellsub; the title attribute
|
|
21999
|
+
// carries the longer, actionable form for hover, so what closes the hole
|
|
22000
|
+
// is available without depending on a pointer to find it.
|
|
22001
|
+
function assumedReachedSub(reason) {
|
|
22002
|
+
var sub = el("span", "cellsub", rerunReasonText("perspectives.rerun.why.", reason));
|
|
22003
|
+
sub.title = rerunReasonText("perspectives.rerun.fix.", reason);
|
|
22004
|
+
return sub;
|
|
22005
|
+
}
|
|
22006
|
+
|
|
22007
|
+
// Axis 1, as the hub derived it against the deployed commit — fresher than
|
|
22008
|
+
// the raw drift ledger, whose entry may predate the deploy. The ledger
|
|
22009
|
+
// survives here only as the "audited at" coordinate (ledgerLine below):
|
|
22010
|
+
// its own finding is superseded by rr.audit/rr.driftLabel above, so
|
|
22011
|
+
// showing both would repeat one answer in two vocabularies — the bug this
|
|
22012
|
+
// column used to have paired with a since-removed fourth column.
|
|
21963
22013
|
var AUDIT_BADGE = {
|
|
21964
|
-
due: "rr-none", clean: "passed", drifted: "failed",
|
|
21965
|
-
undecided: "rr-unknown", cannotTell: "rr-unknown"
|
|
22014
|
+
due: "rr-none", clean: "passed", drifted: "failed", undecided: "rr-unknown"
|
|
21966
22015
|
};
|
|
21967
22016
|
|
|
21968
|
-
function perspAuditCell(rr) {
|
|
22017
|
+
function perspAuditCell(rr, driftEntry) {
|
|
21969
22018
|
var td = el("td");
|
|
21970
22019
|
if (!rr || !rr.audit) {
|
|
21971
22020
|
td.appendChild(el("span", "muted", "\u2014"));
|
|
@@ -21977,6 +22026,23 @@ const CLIENT_JS = `
|
|
|
21977
22026
|
td.appendChild(badge);
|
|
21978
22027
|
if (rr.audit === "drifted" && rr.driftLabel) {
|
|
21979
22028
|
td.appendChild(el("span", "cellsub", labelText(rr.driftLabel)));
|
|
22029
|
+
} else if (rr.auditAssumedReached) {
|
|
22030
|
+
// Due because the log could not place the audit, not because a deploy
|
|
22031
|
+
// demonstrably reached it. Say which, or the whole column reads "due"
|
|
22032
|
+
// with no cause anywhere on the page.
|
|
22033
|
+
td.appendChild(assumedReachedSub(rr.auditAssumedReached));
|
|
22034
|
+
}
|
|
22035
|
+
// "audited at sha · when" — the same freshness evidence the execution
|
|
22036
|
+
// column gives its own last result, below. SpecRerun does not carry the
|
|
22037
|
+
// audit's own coordinate, so it comes from the drift ledger.
|
|
22038
|
+
if (driftEntry) {
|
|
22039
|
+
var sub = el("span", "cellsub");
|
|
22040
|
+
sub.appendChild(ledgerLine(driftEntry));
|
|
22041
|
+
if (driftEntry.graded) {
|
|
22042
|
+
sub.appendChild(document.createTextNode(" · "));
|
|
22043
|
+
sub.appendChild(el("span", "graded-mark", t("perspectives.drift.graded")));
|
|
22044
|
+
}
|
|
22045
|
+
td.appendChild(sub);
|
|
21980
22046
|
}
|
|
21981
22047
|
return td;
|
|
21982
22048
|
}
|
|
@@ -21987,7 +22053,7 @@ const CLIENT_JS = `
|
|
|
21987
22053
|
// No verdict at all: this case is in the document but not in the report
|
|
21988
22054
|
// (added since it was computed). Not the same statement as "never run".
|
|
21989
22055
|
if (!runState) {
|
|
21990
|
-
td.appendChild(el("span", "muted", "
|
|
22056
|
+
td.appendChild(el("span", "muted", "\u2014"));
|
|
21991
22057
|
return td;
|
|
21992
22058
|
}
|
|
21993
22059
|
var badge = el("span", "badge " + RUN_STATE_BADGE[runState]);
|
|
@@ -21995,8 +22061,11 @@ const CLIENT_JS = `
|
|
|
21995
22061
|
badge.appendChild(document.createTextNode(" " + t("perspectives.run.state." + runState)));
|
|
21996
22062
|
td.appendChild(badge);
|
|
21997
22063
|
|
|
21998
|
-
// The sub-line is the coordinate of the result being reported
|
|
21999
|
-
//
|
|
22064
|
+
// The sub-line is the coordinate of the result being reported — the same
|
|
22065
|
+
// "when is this from" evidence the audit column carries above, so the two
|
|
22066
|
+
// axes read as siblings. Why the currency matters belongs to the verdict
|
|
22067
|
+
// column, except for the hole that pinned this to "pending" with no
|
|
22068
|
+
// deploy to point at.
|
|
22000
22069
|
if (runState !== "never") {
|
|
22001
22070
|
var last = lastResult(rr);
|
|
22002
22071
|
if (last) {
|
|
@@ -22005,53 +22074,7 @@ const CLIENT_JS = `
|
|
|
22005
22074
|
td.appendChild(sub);
|
|
22006
22075
|
}
|
|
22007
22076
|
}
|
|
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
|
-
}
|
|
22077
|
+
if (rr.executionAssumedReached) td.appendChild(assumedReachedSub(rr.executionAssumedReached));
|
|
22055
22078
|
return td;
|
|
22056
22079
|
}
|
|
22057
22080
|
|
|
@@ -22105,45 +22128,47 @@ const CLIENT_JS = `
|
|
|
22105
22128
|
return row;
|
|
22106
22129
|
}
|
|
22107
22130
|
|
|
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.
|
|
22131
|
+
// Summary: the inventory as one line, then one row per axis — verdict,
|
|
22132
|
+
// execution, audit, the same three groupings the table's columns show, so
|
|
22133
|
+
// the bars and the table never disagree about what a word means. The mode
|
|
22134
|
+
// and recorded-ness counts are not lost; they moved onto the filter chips,
|
|
22135
|
+
// where a count states what that filter would leave behind.
|
|
22113
22136
|
//
|
|
22114
22137
|
// 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.
|
|
22138
|
+
// has been recorded on) every case is "not evaluated" and all three bars
|
|
22139
|
+
// are omitted together, rather than showing a composition that reads as
|
|
22140
|
+
// "nothing to do".
|
|
22121
22141
|
function renderPerspOverview(doc) {
|
|
22122
22142
|
var host = document.getElementById("persp-ov");
|
|
22123
22143
|
clear(host);
|
|
22124
|
-
var
|
|
22125
|
-
var driftEntries = perspState.drift ? [] : null;
|
|
22144
|
+
var records = [];
|
|
22126
22145
|
doc.features.forEach(function (feature) {
|
|
22127
22146
|
feature.specs.forEach(function (spec) {
|
|
22128
|
-
|
|
22129
|
-
if (driftEntries) driftEntries.push(ledgerEntryFor(perspState.drift, feature, spec));
|
|
22147
|
+
records.push(ledgerEntryFor(perspState.rerun, feature, spec));
|
|
22130
22148
|
});
|
|
22131
22149
|
});
|
|
22132
22150
|
|
|
22133
22151
|
var inv = el("div", "ov-inv");
|
|
22134
|
-
inv.appendChild(el("b", null, String(
|
|
22152
|
+
inv.appendChild(el("b", null, String(records.length)));
|
|
22135
22153
|
inv.appendChild(document.createTextNode(" " + t("perspectives.ov.cases") + " / "));
|
|
22136
22154
|
inv.appendChild(el("b", null, String(doc.features.length)));
|
|
22137
22155
|
inv.appendChild(document.createTextNode(" " + t("perspectives.ov.features")));
|
|
22138
22156
|
host.appendChild(inv);
|
|
22139
|
-
if (!
|
|
22157
|
+
if (!records.length) return;
|
|
22140
22158
|
|
|
22141
|
-
|
|
22142
|
-
|
|
22143
|
-
|
|
22144
|
-
|
|
22159
|
+
// Gated on the report as a whole: with none, none of the three axes has
|
|
22160
|
+
// anything to compose, and an un-composed bar would paint every case as
|
|
22161
|
+
// needing work — an overstatement about the hub rather than about the
|
|
22162
|
+
// specs.
|
|
22163
|
+
if (perspState.rerun) {
|
|
22145
22164
|
host.appendChild(ovAxisRow(
|
|
22146
|
-
t("perspectives.
|
|
22165
|
+
t("perspectives.col.verdict"), rerunSegments(rerunComposition(records)), "perspectives.rerun.state.", records.length,
|
|
22166
|
+
));
|
|
22167
|
+
host.appendChild(ovAxisRow(
|
|
22168
|
+
t("perspectives.col.run"), executionSegments(executionComposition(records)), "perspectives.run.state.", records.length,
|
|
22169
|
+
));
|
|
22170
|
+
host.appendChild(ovAxisRow(
|
|
22171
|
+
t("perspectives.col.audit"), auditSegments(auditComposition(records)), "perspectives.audit.state.", records.length,
|
|
22147
22172
|
));
|
|
22148
22173
|
}
|
|
22149
22174
|
}
|
|
@@ -22155,15 +22180,16 @@ const CLIENT_JS = `
|
|
|
22155
22180
|
function perspMatches(feature, spec, f) {
|
|
22156
22181
|
if (f === "deterministic" && perspMode(spec) !== "deterministic") return false;
|
|
22157
22182
|
if (f === "live" && perspMode(spec) !== "live") return false;
|
|
22158
|
-
|
|
22183
|
+
// The verdict chips (same words as RERUN_ORDER/the 判定 column) share
|
|
22184
|
+
// rerunVerdictOf with the summary bar, so an unrecognised verdict is
|
|
22185
|
+
// "rerunNeeded" in both places rather than matching no chip. With no
|
|
22186
|
+
// report at all there is nothing to filter on, so every chip yields
|
|
22187
|
+
// nothing.
|
|
22188
|
+
if (RERUN_ORDER.indexOf(f) !== -1) {
|
|
22189
|
+
if (!perspState.rerun) return false;
|
|
22159
22190
|
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;
|
|
22191
|
+
if (rerunVerdictOf(rr) !== f) return false;
|
|
22163
22192
|
}
|
|
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
22193
|
if (perspState.q) {
|
|
22168
22194
|
var hay = (spec.title + " " + (spec.summary || "") + " " + spec.specName).toLowerCase();
|
|
22169
22195
|
if (hay.indexOf(perspState.q) === -1) return false;
|
|
@@ -22186,14 +22212,19 @@ const CLIENT_JS = `
|
|
|
22186
22212
|
// lift this region out of the rendered page and run it: which label the
|
|
22187
22213
|
// panel's evidence row wears, and whether it has a failure row at all.
|
|
22188
22214
|
|
|
22189
|
-
//
|
|
22190
|
-
//
|
|
22191
|
-
//
|
|
22192
|
-
|
|
22193
|
-
|
|
22194
|
-
return
|
|
22195
|
-
|
|
22196
|
-
|
|
22215
|
+
// The deploy log answered for this case: the row can show what it holds.
|
|
22216
|
+
// A case the log could not place has no evidence to show even though its
|
|
22217
|
+
// verdict is the same — the assumption is the answer, not a finding.
|
|
22218
|
+
function rerunHasEvidence(rr) {
|
|
22219
|
+
if (rr.verdict === "verified") return true;
|
|
22220
|
+
return rr.verdict === "rerunNeeded" && !rr.executionAssumedReached;
|
|
22221
|
+
}
|
|
22222
|
+
|
|
22223
|
+
// Evidence is labelled by the timeframe it covers; everything else names why
|
|
22224
|
+
// the verdict landed — a different kind of content, and forcing one label
|
|
22225
|
+
// over both would make one of the two read as a lie.
|
|
22226
|
+
function rerunEvidenceLabelKey(rr) {
|
|
22227
|
+
return rerunHasEvidence(rr) ? "perspectives.d.changedSince" : "perspectives.d.whyVerdict";
|
|
22197
22228
|
}
|
|
22198
22229
|
|
|
22199
22230
|
// The failure row points at a run. With no failure there is nothing to point
|
|
@@ -22229,8 +22260,8 @@ const CLIENT_JS = `
|
|
|
22229
22260
|
// The label already states the timeframe, so the value never repeats it.
|
|
22230
22261
|
function rerunEvidenceValue(rr) {
|
|
22231
22262
|
var wrap = el("div");
|
|
22232
|
-
if (rr
|
|
22233
|
-
wrap.appendChild(el("div", "d-prose",
|
|
22263
|
+
if (!rerunHasEvidence(rr)) {
|
|
22264
|
+
wrap.appendChild(el("div", "d-prose", rerunWhyVerdict(rr)));
|
|
22234
22265
|
return wrap;
|
|
22235
22266
|
}
|
|
22236
22267
|
// Both states require a non-empty deploy log, so a head-less report
|
|
@@ -22280,7 +22311,7 @@ const CLIENT_JS = `
|
|
|
22280
22311
|
|
|
22281
22312
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22282
22313
|
if (rr) {
|
|
22283
|
-
row(rerunEvidenceLabelKey(rr
|
|
22314
|
+
row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
|
|
22284
22315
|
if (rerunHasFailure(rr)) row("perspectives.d.lastRed", ledgerLine(rr.lastRed));
|
|
22285
22316
|
}
|
|
22286
22317
|
frag.appendChild(dl);
|
|
@@ -22325,14 +22356,12 @@ const CLIENT_JS = `
|
|
|
22325
22356
|
var tbody = document.getElementById("persp-tbody");
|
|
22326
22357
|
clear(tbody);
|
|
22327
22358
|
// 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
|
|
22359
|
+
// exactly as it was on a hub that cannot answer the re-run question.
|
|
22329
22360
|
var showRerun = perspState.rerun != null;
|
|
22330
|
-
var showDrift = perspState.drift != null;
|
|
22331
22361
|
document.getElementById("persp-th-verdict").hidden = !showRerun;
|
|
22332
22362
|
document.getElementById("persp-th-audit").hidden = !showRerun;
|
|
22333
22363
|
document.getElementById("persp-th-run").hidden = !showRerun;
|
|
22334
|
-
|
|
22335
|
-
var cols = 3 + (showRerun ? 3 : 0) + (showDrift ? 1 : 0);
|
|
22364
|
+
var cols = 3 + (showRerun ? 3 : 0);
|
|
22336
22365
|
var hits = 0;
|
|
22337
22366
|
doc.features.forEach(function (feature) {
|
|
22338
22367
|
var specs = feature.specs.filter(function (s) { return perspMatches(feature, s, perspState.f); });
|
|
@@ -22364,10 +22393,9 @@ const CLIENT_JS = `
|
|
|
22364
22393
|
if (showRerun) {
|
|
22365
22394
|
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
22366
22395
|
row.appendChild(perspVerdictCell(rr));
|
|
22367
|
-
row.appendChild(perspAuditCell(rr));
|
|
22368
22396
|
row.appendChild(perspRunCell(rr));
|
|
22397
|
+
row.appendChild(perspAuditCell(rr, ledgerEntryFor(perspState.drift, feature, spec)));
|
|
22369
22398
|
}
|
|
22370
|
-
if (showDrift) row.appendChild(perspDriftCell(ledgerEntryFor(perspState.drift, feature, spec)));
|
|
22371
22399
|
|
|
22372
22400
|
var chevTd = el("td", "c-chev");
|
|
22373
22401
|
chevTd.appendChild(el("span", "chev-i", "\\u25b6"));
|
|
@@ -22410,21 +22438,17 @@ const CLIENT_JS = `
|
|
|
22410
22438
|
renderPerspTable(doc);
|
|
22411
22439
|
}
|
|
22412
22440
|
|
|
22413
|
-
// The
|
|
22414
|
-
// otherwise
|
|
22415
|
-
// the active filter when the answer came back
|
|
22441
|
+
// The verdict chip group only exists while the hub answers the re-run
|
|
22442
|
+
// question; otherwise every one of them would filter everything away. Drop
|
|
22443
|
+
// back to "all" if one was the active filter when the answer came back
|
|
22444
|
+
// "not supported".
|
|
22416
22445
|
//
|
|
22417
22446
|
// Each chip also carries what it would yield — the mode breakdown the
|
|
22418
22447
|
// summary row used to spend four tiles on.
|
|
22419
22448
|
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";
|
|
22449
|
+
var group = document.getElementById("persp-verdict-chips");
|
|
22450
|
+
group.hidden = perspState.rerunSupported !== true;
|
|
22451
|
+
if (perspState.rerunSupported !== true && RERUN_ORDER.indexOf(perspState.f) !== -1) perspState.f = "all";
|
|
22428
22452
|
document.querySelectorAll("#view-perspectives .fchip").forEach(function (b) {
|
|
22429
22453
|
var f = b.getAttribute("data-f");
|
|
22430
22454
|
b.setAttribute("aria-pressed", String(f === perspState.f));
|
|
@@ -22432,7 +22456,8 @@ const CLIENT_JS = `
|
|
|
22432
22456
|
});
|
|
22433
22457
|
}
|
|
22434
22458
|
|
|
22435
|
-
//
|
|
22459
|
+
// boxId is a parameter rather than hardcoded so a future note box can reuse
|
|
22460
|
+
// this without copying it — today only "persp-rerun-note" calls it.
|
|
22436
22461
|
function setPerspNote(boxId, text, kind) {
|
|
22437
22462
|
var box = document.getElementById(boxId);
|
|
22438
22463
|
box.hidden = !text;
|
|
@@ -22468,7 +22493,6 @@ const CLIENT_JS = `
|
|
|
22468
22493
|
setPerspDeployHead(null);
|
|
22469
22494
|
perspState.rerun = null;
|
|
22470
22495
|
perspState.rerunSupported = null;
|
|
22471
|
-
setPerspNote("persp-drift-note", "");
|
|
22472
22496
|
perspState.drift = null;
|
|
22473
22497
|
syncPerspChips();
|
|
22474
22498
|
fetchPerspectives()
|
|
@@ -22491,9 +22515,9 @@ const CLIENT_JS = `
|
|
|
22491
22515
|
loadRerun().catch(function (err) {
|
|
22492
22516
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.loadFailed") + ": " + err.message, "warn");
|
|
22493
22517
|
}),
|
|
22494
|
-
|
|
22495
|
-
|
|
22496
|
-
}),
|
|
22518
|
+
// Evidence-only (the "audited at" line in the audit column) — a
|
|
22519
|
+
// failed or unsupported fetch just omits that line, no banner.
|
|
22520
|
+
loadDrift().catch(function () {}),
|
|
22497
22521
|
]);
|
|
22498
22522
|
})
|
|
22499
22523
|
.catch(function (err) {
|
|
@@ -22506,6 +22530,82 @@ const CLIENT_JS = `
|
|
|
22506
22530
|
return state.project + "/" + state.profile;
|
|
22507
22531
|
}
|
|
22508
22532
|
|
|
22533
|
+
// --- pure: data-profile pick ---------------------------------------------
|
|
22534
|
+
// Self-contained on purpose (no DOM, no closures): the network probing this
|
|
22535
|
+
// feeds is what makes "has deploy data" answerable at all, but which
|
|
22536
|
+
// candidate wins from the results is a plain function worth pinning without
|
|
22537
|
+
// mocking a fetch.
|
|
22538
|
+
|
|
22539
|
+
// "default" is always offered (the API guarantees it) but usually holds no
|
|
22540
|
+
// deploys, so opening straight into it reads as broken: every row pending,
|
|
22541
|
+
// a "no deploy log" banner. Fallback for when no candidate's deploy log can
|
|
22542
|
+
// be confirmed (a fresh project): prefer a profile the run index shows has
|
|
22543
|
+
// runs, with exactly one such profile the only reasonable pick.
|
|
22544
|
+
function pickDataProfile(current, dataProfiles) {
|
|
22545
|
+
if (!dataProfiles.length || dataProfiles.indexOf(current) !== -1) return current;
|
|
22546
|
+
return dataProfiles.length === 1 ? dataProfiles[0] : dataProfiles.slice().sort()[0];
|
|
22547
|
+
}
|
|
22548
|
+
|
|
22549
|
+
// Every profile worth checking for deploy data, in preference order: the
|
|
22550
|
+
// current profile first (so an already-fine pick is left alone), then
|
|
22551
|
+
// run-index profiles (a profile with runs is a maintained environment),
|
|
22552
|
+
// then the project's full profile set. That last tier is what makes a
|
|
22553
|
+
// profile deploys are recorded under but nothing has ever run against
|
|
22554
|
+
// reachable at all — the run index alone has no way to see it.
|
|
22555
|
+
function dataProfileCandidates(current, dataProfiles, projectProfiles) {
|
|
22556
|
+
var seen = {};
|
|
22557
|
+
var out = [];
|
|
22558
|
+
[current].concat(dataProfiles, projectProfiles).forEach(function (p) {
|
|
22559
|
+
if (p && !seen[p]) { seen[p] = true; out.push(p); }
|
|
22560
|
+
});
|
|
22561
|
+
return out;
|
|
22562
|
+
}
|
|
22563
|
+
|
|
22564
|
+
// The deterministic half of resolveDataProfile below: given which
|
|
22565
|
+
// candidates actually have a deploy log (probed in parallel, so answers can
|
|
22566
|
+
// arrive in any order), pick the first by candidate order, not by response
|
|
22567
|
+
// order — the result must not depend on network timing. Falls back to
|
|
22568
|
+
// pickDataProfile's answer when nothing confirms (no project has deploy
|
|
22569
|
+
// data yet), leaving that case's behaviour unchanged.
|
|
22570
|
+
function pickFirstWithDeployLog(candidates, hasLog, current, dataProfiles) {
|
|
22571
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
22572
|
+
if (hasLog[i]) return candidates[i];
|
|
22573
|
+
}
|
|
22574
|
+
return pickDataProfile(current, dataProfiles);
|
|
22575
|
+
}
|
|
22576
|
+
// --- end pure: data-profile pick ------------------------------------------
|
|
22577
|
+
|
|
22578
|
+
// Confirms a candidate actually has a deploy log, rather than merely being
|
|
22579
|
+
// known to the run index or the secrets tab — the two facts pickDataProfile
|
|
22580
|
+
// used to conflate (a profile can hold runs, or secrets, with no deploy log
|
|
22581
|
+
// at all). limit=1 makes this a cheap existence check.
|
|
22582
|
+
function hasDeployLog(profile) {
|
|
22583
|
+
return apiFetch(
|
|
22584
|
+
"/api/v1/projects/" + encodeURIComponent(state.project) + "/deploys?profile=" + encodeURIComponent(profile) + "&limit=1",
|
|
22585
|
+
).then(function (data) { return !!(data && data.entries && data.entries.length); })
|
|
22586
|
+
.catch(function () { return false; });
|
|
22587
|
+
}
|
|
22588
|
+
|
|
22589
|
+
// The candidate set from /profiles is broader than the run index (see
|
|
22590
|
+
// dataProfileCandidates), so probe every candidate's deploy log in
|
|
22591
|
+
// parallel before letting pickFirstWithDeployLog decide.
|
|
22592
|
+
function resolveDataProfile(current, dataProfiles, projectProfiles) {
|
|
22593
|
+
var candidates = dataProfileCandidates(current, dataProfiles, projectProfiles);
|
|
22594
|
+
return Promise.all(candidates.map(hasDeployLog))
|
|
22595
|
+
.then(function (hasLog) { return pickFirstWithDeployLog(candidates, hasLog, current, dataProfiles); });
|
|
22596
|
+
}
|
|
22597
|
+
|
|
22598
|
+
// The full profile set (secrets tab's universe) is broader than the run
|
|
22599
|
+
// index: a profile deploys are recorded under but nothing has ever run
|
|
22600
|
+
// against is invisible to fetchRunIndex. Fetched independently from
|
|
22601
|
+
// loadProfiles, which mutates knownProfiles/state.profile as a side effect
|
|
22602
|
+
// the Secrets tab depends on and this must not trigger.
|
|
22603
|
+
function fetchProjectProfiles() {
|
|
22604
|
+
return apiFetch("/api/v1/projects/" + encodeURIComponent(state.project) + "/profiles")
|
|
22605
|
+
.then(function (data) { return (data && data.profiles) || []; })
|
|
22606
|
+
.catch(function () { return []; });
|
|
22607
|
+
}
|
|
22608
|
+
|
|
22509
22609
|
// Project-scoped (see fetchRunIndex) — never rejects, so this always
|
|
22510
22610
|
// re-renders once the run index settles, whichever of the three loads in
|
|
22511
22611
|
// loadPerspectives is slowest.
|
|
@@ -22513,7 +22613,22 @@ const CLIENT_JS = `
|
|
|
22513
22613
|
return fetchRunIndex().then(function (result) {
|
|
22514
22614
|
perspState.runUrls = result.urls;
|
|
22515
22615
|
perspState.rerunProfiles = result.profiles;
|
|
22516
|
-
|
|
22616
|
+
if (storedProfileForProject(state.project)) {
|
|
22617
|
+
renderPerspectives();
|
|
22618
|
+
return;
|
|
22619
|
+
}
|
|
22620
|
+
// Never overrides a choice the user made explicitly (the check above)
|
|
22621
|
+
// — this only fills in the very first, unopinionated default.
|
|
22622
|
+
return fetchProjectProfiles()
|
|
22623
|
+
.then(function (projectProfiles) { return resolveDataProfile(state.profile, result.profiles, projectProfiles); })
|
|
22624
|
+
.then(function (pick) {
|
|
22625
|
+
if (pick !== state.profile) {
|
|
22626
|
+
setProfile(pick);
|
|
22627
|
+
renderPerspectives();
|
|
22628
|
+
return reloadRerun();
|
|
22629
|
+
}
|
|
22630
|
+
renderPerspectives();
|
|
22631
|
+
});
|
|
22517
22632
|
});
|
|
22518
22633
|
}
|
|
22519
22634
|
|
|
@@ -22529,8 +22644,9 @@ const CLIENT_JS = `
|
|
|
22529
22644
|
if (rerun.note) {
|
|
22530
22645
|
setPerspNote("persp-rerun-note", rerun.note, rerun.kind);
|
|
22531
22646
|
} else if (perspState.rerun && !perspState.rerun.deployHead) {
|
|
22532
|
-
//
|
|
22533
|
-
// is missing and how to supply it,
|
|
22647
|
+
// With no deploy log nothing can be placed, so every case is assumed
|
|
22648
|
+
// reached. Say once, at the top, what is missing and how to supply it,
|
|
22649
|
+
// rather than repeating it on every row.
|
|
22534
22650
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.noDeployLogBanner").replace("{profile}", state.profile), "warn");
|
|
22535
22651
|
} else {
|
|
22536
22652
|
setPerspNote("persp-rerun-note", "");
|
|
@@ -22540,13 +22656,18 @@ const CLIENT_JS = `
|
|
|
22540
22656
|
}
|
|
22541
22657
|
|
|
22542
22658
|
// Loaded once per project open — never re-run on a profile switch, since
|
|
22543
|
-
// drift carries no profile (unlike loadRerun/reloadRerun below).
|
|
22659
|
+
// drift carries no profile (unlike loadRerun/reloadRerun below). Only the
|
|
22660
|
+
// ledger's own coordinate (when a spec was last audited) survives into the
|
|
22661
|
+
// view now — its finding is superseded by the fresher, deploy-aware audit
|
|
22662
|
+
// axis in the /rerun report — so there is nothing here worth a banner on
|
|
22663
|
+
// an older or unreachable hub.
|
|
22544
22664
|
function loadDrift() {
|
|
22545
|
-
return
|
|
22546
|
-
|
|
22547
|
-
|
|
22548
|
-
|
|
22549
|
-
|
|
22665
|
+
return fetch(driftPath(), { headers: { Authorization: "Bearer " + state.token } })
|
|
22666
|
+
.then(function (res) { return res.ok ? res.json() : null; }, function () { return null; })
|
|
22667
|
+
.then(function (report) {
|
|
22668
|
+
perspState.drift = report || null;
|
|
22669
|
+
renderPerspectives();
|
|
22670
|
+
});
|
|
22550
22671
|
}
|
|
22551
22672
|
|
|
22552
22673
|
// Switching profile re-asks only the profile-scoped question: the
|