ccqa 1.32.0 → 1.34.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/dist/bin/ccqa.mjs +704 -229
- package/dist/hub-client/index.d.mts +60 -0
- package/dist/hub-client/index.mjs +20 -0
- package/dist/package.json +1 -1
- package/package.json +1 -1
package/dist/bin/ccqa.mjs
CHANGED
|
@@ -1524,16 +1524,21 @@ const ENDPOINT_ENV_KEYS = [
|
|
|
1524
1524
|
"CLAUDE_CODE_OAUTH_TOKEN"
|
|
1525
1525
|
];
|
|
1526
1526
|
/**
|
|
1527
|
+
* When both credentials are present the OAuth token wins and the API key is
|
|
1528
|
+
* dropped. Left to the CLI the API key would win, which makes "switch a CI
|
|
1529
|
+
* job to the subscription token" require unwiring the key everywhere; with
|
|
1530
|
+
* this rule, adding the one variable is the whole switch, and removing it is
|
|
1531
|
+
* the whole rollback. The one place the rule lives — both the resolved view
|
|
1532
|
+
* and the env the SDK receives apply it through here.
|
|
1533
|
+
*/
|
|
1534
|
+
function preferOauthToken(env) {
|
|
1535
|
+
if (env["CLAUDE_CODE_OAUTH_TOKEN"]) delete env["ANTHROPIC_API_KEY"];
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1527
1538
|
* Collects the endpoint/auth variables set in the current process environment
|
|
1528
1539
|
* so they can be forwarded, verbatim, to every Claude Code invocation. Returns
|
|
1529
1540
|
* only the keys that are actually set (non-empty), so unset variables never
|
|
1530
|
-
* override the SDK's own defaults.
|
|
1531
|
-
*
|
|
1532
|
-
* When both credentials are present the OAuth token wins and the API key is
|
|
1533
|
-
* not forwarded. Left to the CLI the API key would win, which makes "switch a
|
|
1534
|
-
* CI job to the subscription token" require unwiring the key everywhere; with
|
|
1535
|
-
* the precedence here, adding the one variable is the whole switch, and
|
|
1536
|
-
* removing it is the whole rollback.
|
|
1541
|
+
* override the SDK's own defaults. Credential precedence per preferOauthToken.
|
|
1537
1542
|
*/
|
|
1538
1543
|
function resolveEndpointEnv() {
|
|
1539
1544
|
const endpointEnv = {};
|
|
@@ -1541,7 +1546,7 @@ function resolveEndpointEnv() {
|
|
|
1541
1546
|
const value = process.env[key];
|
|
1542
1547
|
if (value && value.length > 0) endpointEnv[key] = value;
|
|
1543
1548
|
}
|
|
1544
|
-
|
|
1549
|
+
preferOauthToken(endpointEnv);
|
|
1545
1550
|
return endpointEnv;
|
|
1546
1551
|
}
|
|
1547
1552
|
/**
|
|
@@ -1555,6 +1560,30 @@ function withoutEmptyEndpointVars(env) {
|
|
|
1555
1560
|
for (const key of ENDPOINT_ENV_KEYS) if (out[key] === "") delete out[key];
|
|
1556
1561
|
return out;
|
|
1557
1562
|
}
|
|
1563
|
+
/**
|
|
1564
|
+
* The environment actually handed to the Claude Code process: the full process
|
|
1565
|
+
* environment with the caller's overrides on top, empty endpoint variables
|
|
1566
|
+
* dropped, and — when both credentials survive the merge — the API key removed
|
|
1567
|
+
* so the OAuth token wins.
|
|
1568
|
+
*
|
|
1569
|
+
* That removal MUST happen on the env the SDK receives, not only on the
|
|
1570
|
+
* resolved view: left to the CLI the API key would win, silently moving every
|
|
1571
|
+
* call from the subscription to metered billing when a CI job wires both
|
|
1572
|
+
* (which is exactly what happened before this function existed).
|
|
1573
|
+
*
|
|
1574
|
+
* Returns undefined when no endpoint variable is set and the caller passes no
|
|
1575
|
+
* env, so the SDK keeps its own default environment.
|
|
1576
|
+
*/
|
|
1577
|
+
function buildInvocationEnv(env) {
|
|
1578
|
+
const hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
|
|
1579
|
+
if (!env && !hasEndpointEnv) return void 0;
|
|
1580
|
+
const merged = withoutEmptyEndpointVars({
|
|
1581
|
+
...process.env,
|
|
1582
|
+
...env
|
|
1583
|
+
});
|
|
1584
|
+
preferOauthToken(merged);
|
|
1585
|
+
return merged;
|
|
1586
|
+
}
|
|
1558
1587
|
let nativeBinaryWarned = false;
|
|
1559
1588
|
/**
|
|
1560
1589
|
* Warn once per process when the SDK's per-platform native binary is missing:
|
|
@@ -1570,11 +1599,7 @@ function warnOnceIfNativeBinaryMissing() {
|
|
|
1570
1599
|
async function invokeClaudeStreaming(options, onEvent) {
|
|
1571
1600
|
const { prompt, systemPrompt, allowedTools, disableBuiltinTools = false, disableThinking = false, mcpServers, maxTurns, env, model, cwd, onAbAction, onAbActionFailed, silenceBashLog = false, envScrubMap = [], relaxAbConstraints = false } = options;
|
|
1572
1601
|
const resolvedModel = resolveModel(model);
|
|
1573
|
-
const
|
|
1574
|
-
const mergedEnv = env || hasEndpointEnv ? withoutEmptyEndpointVars({
|
|
1575
|
-
...process.env,
|
|
1576
|
-
...env
|
|
1577
|
-
}) : void 0;
|
|
1602
|
+
const mergedEnv = buildInvocationEnv(env);
|
|
1578
1603
|
let lastAbToolUseId = null;
|
|
1579
1604
|
const claimAbToolUse = (toolUseId) => {
|
|
1580
1605
|
if (toolUseId !== lastAbToolUseId) return false;
|
|
@@ -7518,6 +7543,46 @@ z.object({
|
|
|
7518
7543
|
specs: z.record(z.string(), AttestationSchema)
|
|
7519
7544
|
});
|
|
7520
7545
|
/**
|
|
7546
|
+
* A person's answer to one audit finding: the spec describes the code fine,
|
|
7547
|
+
* and the finding is wrong. Where an attestation speaks about the product,
|
|
7548
|
+
* this speaks about the *audit* — so it settles the audit axis rather than
|
|
7549
|
+
* the verdict, and the spec goes back to being run like any other.
|
|
7550
|
+
*
|
|
7551
|
+
* Pinned to the audit run whose finding it answers. A later audit is a new
|
|
7552
|
+
* observation of newer code, so it produces a new run and this stops
|
|
7553
|
+
* applying: the machine gets to raise the finding again, and the record of
|
|
7554
|
+
* the last dismissal is shown beside it rather than silently suppressing it.
|
|
7555
|
+
* No profile — an audit finding is about the repository, not an environment
|
|
7556
|
+
* (ADR-0013), which is also why this is scoped per project alone.
|
|
7557
|
+
*/
|
|
7558
|
+
const AuditDismissalSchema = z.object({
|
|
7559
|
+
by: z.string().min(1),
|
|
7560
|
+
at: z.string(),
|
|
7561
|
+
note: z.string().min(1),
|
|
7562
|
+
auditRunId: z.string(),
|
|
7563
|
+
label: DriftLabelSchema,
|
|
7564
|
+
headline: z.string()
|
|
7565
|
+
});
|
|
7566
|
+
/** The per-project dismissal document: "feature/spec" → the last dismissal. */
|
|
7567
|
+
const AuditDismissalsSchema = z.object({ specs: z.record(z.string(), AuditDismissalSchema).default({}) });
|
|
7568
|
+
/** Body of `PUT /projects/:project/audit-dismissals`. */
|
|
7569
|
+
const PutAuditDismissalRequestSchema = z.object({
|
|
7570
|
+
spec: z.string().min(1).max(512),
|
|
7571
|
+
by: z.string().min(1).max(256),
|
|
7572
|
+
note: z.string().min(1).max(4e3)
|
|
7573
|
+
});
|
|
7574
|
+
/** Body of `DELETE /projects/:project/audit-dismissals`. */
|
|
7575
|
+
const DeleteAuditDismissalRequestSchema = z.object({ spec: z.string().min(1).max(512) });
|
|
7576
|
+
z.object({
|
|
7577
|
+
project: z.string(),
|
|
7578
|
+
spec: z.string(),
|
|
7579
|
+
dismissal: AuditDismissalSchema
|
|
7580
|
+
});
|
|
7581
|
+
z.object({
|
|
7582
|
+
project: z.string(),
|
|
7583
|
+
specs: z.record(z.string(), AuditDismissalSchema)
|
|
7584
|
+
});
|
|
7585
|
+
/**
|
|
7521
7586
|
* One spec's verdict, the two axes it was derived from, and the three ledger
|
|
7522
7587
|
* coordinates the view shows alongside them. The coordinates are always
|
|
7523
7588
|
* present (null when the spec has no such entry); the optional fields appear
|
|
@@ -7533,6 +7598,8 @@ const SpecRerunSchema = z.object({
|
|
|
7533
7598
|
execution: ExecutionStateSchema,
|
|
7534
7599
|
driftLabel: DriftLabelSchema.exclude(["UNKNOWN"]).optional(),
|
|
7535
7600
|
auditAssumedReached: RerunUnknownReasonSchema.optional(),
|
|
7601
|
+
auditDismissed: AuditDismissalSchema.optional(),
|
|
7602
|
+
auditDismissalApplied: z.boolean().optional(),
|
|
7536
7603
|
executionAssumedReached: RerunUnknownReasonSchema.optional(),
|
|
7537
7604
|
specChangedSince: z.string().optional(),
|
|
7538
7605
|
manual: AttestationSchema.optional(),
|
|
@@ -9297,6 +9364,20 @@ function connect(opts) {
|
|
|
9297
9364
|
error("hub token is required (--hub-token or CCQA_HUB_TOKEN)");
|
|
9298
9365
|
process.exit(2);
|
|
9299
9366
|
}
|
|
9367
|
+
/**
|
|
9368
|
+
* The canonical "feature/spec" key for a CLI argument. Hub records are stored
|
|
9369
|
+
* and looked up under exactly this form, so an alias accepted here but kept
|
|
9370
|
+
* verbatim would silently never match.
|
|
9371
|
+
*/
|
|
9372
|
+
function requireSpecId(rawSpecId) {
|
|
9373
|
+
try {
|
|
9374
|
+
const parsed = parseSpecPath(rawSpecId);
|
|
9375
|
+
return `${parsed.featureName}/${parsed.specName}`;
|
|
9376
|
+
} catch (err) {
|
|
9377
|
+
error(errMessage(err));
|
|
9378
|
+
process.exit(2);
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9300
9381
|
function validateSessionName(name) {
|
|
9301
9382
|
const parsed = SessionNameSchema.safeParse(name);
|
|
9302
9383
|
if (!parsed.success) {
|
|
@@ -9594,14 +9675,7 @@ const pushCommand = new Command("push").description("Upload the report directory
|
|
|
9594
9675
|
const attestCommand = new Command("attest").argument("<feature/spec>", "Spec id, e.g. checkout/happy-path").description("Record that a person checked a spec's behaviour by hand against the deployed environment. The verdict answers manuallyVerified instead of asking a person for what a person already did — the drift ledger is untouched, so the repair loop keeps its reason to fix the test. The attestation lapses on its own when a deploy reaches the spec or the spec is edited.").requiredOption("--profile <name>", "Environment that was checked (e.g. 'stg'). The attestation is anchored to its current deploy head.").option("--by <name>", "Who checked. Required unless --revoke.").option("--note <text>", "What was checked and how — the reader deciding whether to trust it sees this.").option("--revoke", "Withdraw the spec's attestation instead of recording one.").option(...hubUrlOption).option(...hubTokenOption).option("--project <name>", "Hub project. Defaults to the current directory's name.").option("--cwd <path>", "Directory the default --project name is resolved against.").action(withHubErrors(async (rawSpecId, opts) => {
|
|
9595
9676
|
const project = resolveProject(opts);
|
|
9596
9677
|
const hub = connect(opts);
|
|
9597
|
-
|
|
9598
|
-
try {
|
|
9599
|
-
const parsed = parseSpecPath(rawSpecId);
|
|
9600
|
-
specId = `${parsed.featureName}/${parsed.specName}`;
|
|
9601
|
-
} catch (err) {
|
|
9602
|
-
error(errMessage(err));
|
|
9603
|
-
process.exit(2);
|
|
9604
|
-
}
|
|
9678
|
+
const specId = requireSpecId(rawSpecId);
|
|
9605
9679
|
if (opts.revoke) {
|
|
9606
9680
|
await hub.deleteAttestation(project, { profile: opts.profile }, specId);
|
|
9607
9681
|
header("hub attest", `${specId} revoked`);
|
|
@@ -9621,7 +9695,30 @@ const attestCommand = new Command("attest").argument("<feature/spec>", "Spec id,
|
|
|
9621
9695
|
meta("anchored to deploy", res.attestation.deployedSha ?? "(no deploy log)");
|
|
9622
9696
|
info("the verdict answers manuallyVerified until a deploy reaches this spec or the spec is edited");
|
|
9623
9697
|
}));
|
|
9624
|
-
const
|
|
9698
|
+
const dismissCommand = new Command("dismiss").argument("<feature/spec>", "Spec id, e.g. checkout/happy-path").description("Record that a person judged the spec's current audit finding wrong: the spec describes the code fine. This settles the audit axis rather than the verdict — the spec goes back to being run like any other, and the next run says whether the person was right. The dismissal is pinned to the audit run that raised the finding, so a later audit can raise it again. No --profile: a finding is about the repository, not an environment.").option("--by <name>", "Who judged it wrong. Required unless --revoke.").option("--reason <text>", "Why the finding is wrong. Required unless --revoke — this is what a mis-firing audit learns from.").option("--revoke", "Withdraw the dismissal, putting the finding back in force.").option(...hubUrlOption).option(...hubTokenOption).option("--project <name>", "Hub project. Defaults to the current directory's name.").option("--cwd <path>", "Directory the default --project name is resolved against.").action(withHubErrors(async (rawSpecId, opts) => {
|
|
9699
|
+
const project = resolveProject(opts);
|
|
9700
|
+
const hub = connect(opts);
|
|
9701
|
+
const specId = requireSpecId(rawSpecId);
|
|
9702
|
+
if (opts.revoke) {
|
|
9703
|
+
await hub.deleteAuditDismissal(project, specId);
|
|
9704
|
+
header("hub dismiss", `${specId} revoked`);
|
|
9705
|
+
return;
|
|
9706
|
+
}
|
|
9707
|
+
if (!opts.by || !opts.reason) {
|
|
9708
|
+
error("--by <name> and --reason <text> are both required: a dismissal is a person's correction, and it needs the person and the correction");
|
|
9709
|
+
process.exit(2);
|
|
9710
|
+
}
|
|
9711
|
+
const res = await hub.putAuditDismissal(project, {
|
|
9712
|
+
spec: specId,
|
|
9713
|
+
by: opts.by,
|
|
9714
|
+
note: opts.reason
|
|
9715
|
+
});
|
|
9716
|
+
header("hub dismiss", specId);
|
|
9717
|
+
meta("by", res.dismissal.by);
|
|
9718
|
+
meta("dismissed", `${res.dismissal.label} — ${res.dismissal.headline || "(no headline)"}`);
|
|
9719
|
+
info("this finding no longer holds the spec back; a later audit can raise one of its own");
|
|
9720
|
+
}));
|
|
9721
|
+
const hubCommand = new Command("hub").description("Client for a ccqa hub: push run results and manage sessions/variables/prompts used by `ccqa run`. See docs/hub.md.").addCommand(pushCommand).addCommand(deployCommand).addCommand(costCommand).addCommand(sessionCommand).addCommand(varCommand).addCommand(promptCommand).addCommand(attestCommand).addCommand(dismissCommand);
|
|
9625
9722
|
/** Loose check that a fetched session is agent-browser storage-state, mirroring loadStorageState. */
|
|
9626
9723
|
function isStorageStateShape(state) {
|
|
9627
9724
|
return typeof state === "object" && state !== null && Array.isArray(state.cookies) && Array.isArray(state.origins);
|
|
@@ -18929,7 +19026,7 @@ function createGetAuditNeedHandler(storage) {
|
|
|
18929
19026
|
//#endregion
|
|
18930
19027
|
//#region src/hub/api/handlers/locks.ts
|
|
18931
19028
|
/** A spec-key list and three short strings; nothing here should approach this. */
|
|
18932
|
-
const MAX_BODY_BYTES$
|
|
19029
|
+
const MAX_BODY_BYTES$5 = 1024 * 1024;
|
|
18933
19030
|
/**
|
|
18934
19031
|
* POST /api/v1/projects/:project/locks?profile=
|
|
18935
19032
|
*
|
|
@@ -18943,7 +19040,7 @@ function createAcquireLocksHandler(storage) {
|
|
|
18943
19040
|
return async (ctx) => {
|
|
18944
19041
|
const project = requireSafeSegment(ctx.params.project, "project");
|
|
18945
19042
|
const profile = requireProfileParam(ctx.url);
|
|
18946
|
-
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$
|
|
19043
|
+
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$5, AcquireLocksRequestSchema, "lock request");
|
|
18947
19044
|
let result = {
|
|
18948
19045
|
granted: [],
|
|
18949
19046
|
denied: []
|
|
@@ -18972,7 +19069,7 @@ function createReleaseLocksHandler(storage) {
|
|
|
18972
19069
|
return async (ctx) => {
|
|
18973
19070
|
const project = requireSafeSegment(ctx.params.project, "project");
|
|
18974
19071
|
const profile = requireProfileParam(ctx.url);
|
|
18975
|
-
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$
|
|
19072
|
+
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$5, ReleaseLocksRequestSchema, "release request");
|
|
18976
19073
|
await storage.locks.update(project, profile, (current) => releaseAll(current, body.holder));
|
|
18977
19074
|
ctx.res.writeHead(204).end();
|
|
18978
19075
|
};
|
|
@@ -18985,7 +19082,7 @@ function createReleaseLocksHandler(storage) {
|
|
|
18985
19082
|
* 5000 keys of 256 `\uXXXX`-escaped characters — so a conforming client is
|
|
18986
19083
|
* never answered 413 by a limit the documented bounds don't mention.
|
|
18987
19084
|
*/
|
|
18988
|
-
const MAX_BODY_BYTES$
|
|
19085
|
+
const MAX_BODY_BYTES$4 = 8 * 1024 * 1024;
|
|
18989
19086
|
function requireAckKey(ctx) {
|
|
18990
19087
|
return {
|
|
18991
19088
|
project: requireSafeSegment(ctx.params.project, "project"),
|
|
@@ -19012,7 +19109,7 @@ function createGetAckHandler(storage) {
|
|
|
19012
19109
|
function createPutAckHandler(storage) {
|
|
19013
19110
|
return async (ctx) => {
|
|
19014
19111
|
const key = requireAckKey(ctx);
|
|
19015
|
-
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$
|
|
19112
|
+
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$4, PutAckRequestSchema, "ack body");
|
|
19016
19113
|
const ack = await storage.acks.put(key.project, key.profile, key.name, body.keys);
|
|
19017
19114
|
sendJson(ctx.res, 200, {
|
|
19018
19115
|
...key,
|
|
@@ -19023,7 +19120,7 @@ function createPutAckHandler(storage) {
|
|
|
19023
19120
|
//#endregion
|
|
19024
19121
|
//#region src/hub/api/handlers/attestations.ts
|
|
19025
19122
|
/** Far above the largest body `PutAttestationRequestSchema`'s bounds admit. */
|
|
19026
|
-
const MAX_BODY_BYTES$
|
|
19123
|
+
const MAX_BODY_BYTES$3 = 64 * 1024;
|
|
19027
19124
|
function requireScope(ctx) {
|
|
19028
19125
|
return {
|
|
19029
19126
|
project: requireSafeSegment(ctx.params.project, "project"),
|
|
@@ -19056,7 +19153,7 @@ function createPutAttestationHandler(storage) {
|
|
|
19056
19153
|
return async (ctx) => {
|
|
19057
19154
|
const scope = requireScope(ctx);
|
|
19058
19155
|
const [body, head, targets] = await Promise.all([
|
|
19059
|
-
readJsonBody(ctx.req, MAX_BODY_BYTES$
|
|
19156
|
+
readJsonBody(ctx.req, MAX_BODY_BYTES$3, PutAttestationRequestSchema, "attestation body"),
|
|
19060
19157
|
storage.deploys.head(scope.project, scope.profile),
|
|
19061
19158
|
requireSpecTargets(storage.perspectives, scope.project, "what can be attested")
|
|
19062
19159
|
]);
|
|
@@ -19086,7 +19183,7 @@ function createPutAttestationHandler(storage) {
|
|
|
19086
19183
|
function createDeleteAttestationHandler(storage) {
|
|
19087
19184
|
return async (ctx) => {
|
|
19088
19185
|
const scope = requireScope(ctx);
|
|
19089
|
-
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$
|
|
19186
|
+
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$3, DeleteAttestationRequestSchema, "attestation body");
|
|
19090
19187
|
await storage.attestations.update(scope.project, scope.profile, (current) => {
|
|
19091
19188
|
const { [body.spec]: _, ...rest } = current.specs;
|
|
19092
19189
|
return { specs: rest };
|
|
@@ -19095,6 +19192,83 @@ function createDeleteAttestationHandler(storage) {
|
|
|
19095
19192
|
};
|
|
19096
19193
|
}
|
|
19097
19194
|
//#endregion
|
|
19195
|
+
//#region src/hub/api/handlers/audit-dismissals.ts
|
|
19196
|
+
/** Far above the largest body `PutAuditDismissalRequestSchema`'s bounds admit. */
|
|
19197
|
+
const MAX_BODY_BYTES$2 = 64 * 1024;
|
|
19198
|
+
/**
|
|
19199
|
+
* GET /api/v1/projects/:project/audit-dismissals — the raw document,
|
|
19200
|
+
* whether or not each entry still answers the spec's current finding. No
|
|
19201
|
+
* `?profile=`: a finding is about the repository (see `AuditDismissalSchema`).
|
|
19202
|
+
*/
|
|
19203
|
+
function createGetAuditDismissalsHandler(storage) {
|
|
19204
|
+
return async (ctx) => {
|
|
19205
|
+
const project = requireSafeSegment(ctx.params.project, "project");
|
|
19206
|
+
const doc = await storage.auditDismissals.get(project);
|
|
19207
|
+
sendJson(ctx.res, 200, {
|
|
19208
|
+
project,
|
|
19209
|
+
specs: doc.specs
|
|
19210
|
+
});
|
|
19211
|
+
};
|
|
19212
|
+
}
|
|
19213
|
+
/**
|
|
19214
|
+
* PUT /api/v1/projects/:project/audit-dismissals — record that a person
|
|
19215
|
+
* judged the spec's current audit finding wrong.
|
|
19216
|
+
*
|
|
19217
|
+
* The finding being answered is read from the ledger rather than taken from
|
|
19218
|
+
* the caller: a dismissal must name the run and the label it answers, and
|
|
19219
|
+
* only the hub knows which finding is current. A spec with no open finding is
|
|
19220
|
+
* a 400 — there is nothing to dismiss, and accepting it would write a record
|
|
19221
|
+
* that never applies to anything.
|
|
19222
|
+
*
|
|
19223
|
+
* The guard stops there on purpose. `/rerun` applies a dismissal only while
|
|
19224
|
+
* the audit is also *current* for the profile being asked about, and that is
|
|
19225
|
+
* a per-profile question this endpoint has no profile to ask it of (a finding
|
|
19226
|
+
* is about the repository, so the dismissal is project-scoped). A dismissal
|
|
19227
|
+
* written while a deploy has overtaken the audit is harmless: the next audit
|
|
19228
|
+
* supersedes the finding, and the record with it.
|
|
19229
|
+
*/
|
|
19230
|
+
function createPutAuditDismissalHandler(storage) {
|
|
19231
|
+
return async (ctx) => {
|
|
19232
|
+
const project = requireSafeSegment(ctx.params.project, "project");
|
|
19233
|
+
const [body, ledger] = await Promise.all([readJsonBody(ctx.req, MAX_BODY_BYTES$2, PutAuditDismissalRequestSchema, "dismissal body"), storage.driftLedger.getMerged(project)]);
|
|
19234
|
+
const entry = ledger.specs[body.spec];
|
|
19235
|
+
if (!entry || entry.label === null) throw new HttpError(400, "no_open_finding", `spec "${body.spec}" has no open audit finding in project "${project}" — there is nothing to dismiss`);
|
|
19236
|
+
const dismissal = {
|
|
19237
|
+
by: body.by,
|
|
19238
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
19239
|
+
note: body.note,
|
|
19240
|
+
auditRunId: entry.runId,
|
|
19241
|
+
label: entry.label,
|
|
19242
|
+
headline: entry.headline ?? ""
|
|
19243
|
+
};
|
|
19244
|
+
await storage.auditDismissals.update(project, (current) => ({ specs: {
|
|
19245
|
+
...current.specs,
|
|
19246
|
+
[body.spec]: dismissal
|
|
19247
|
+
} }));
|
|
19248
|
+
sendJson(ctx.res, 200, {
|
|
19249
|
+
project,
|
|
19250
|
+
spec: body.spec,
|
|
19251
|
+
dismissal
|
|
19252
|
+
});
|
|
19253
|
+
};
|
|
19254
|
+
}
|
|
19255
|
+
/**
|
|
19256
|
+
* DELETE /api/v1/projects/:project/audit-dismissals — withdraw a dismissal,
|
|
19257
|
+
* putting the audit's finding back in force. Deleting one that does not exist
|
|
19258
|
+
* is 200: the caller asked for its absence, and it is absent.
|
|
19259
|
+
*/
|
|
19260
|
+
function createDeleteAuditDismissalHandler(storage) {
|
|
19261
|
+
return async (ctx) => {
|
|
19262
|
+
const project = requireSafeSegment(ctx.params.project, "project");
|
|
19263
|
+
const body = await readJsonBody(ctx.req, MAX_BODY_BYTES$2, DeleteAuditDismissalRequestSchema, "dismissal body");
|
|
19264
|
+
await storage.auditDismissals.update(project, (current) => {
|
|
19265
|
+
const { [body.spec]: _, ...rest } = current.specs;
|
|
19266
|
+
return { specs: rest };
|
|
19267
|
+
});
|
|
19268
|
+
sendJson(ctx.res, 200, { removed: body.spec });
|
|
19269
|
+
};
|
|
19270
|
+
}
|
|
19271
|
+
//#endregion
|
|
19098
19272
|
//#region src/hub/api/handlers/spend.ts
|
|
19099
19273
|
/** One entry is a handful of short fields; anything larger is a malformed client. */
|
|
19100
19274
|
const MAX_BODY_BYTES$1 = 4 * 1024;
|
|
@@ -19167,7 +19341,7 @@ function specMovedSince(changedAt, baselineSha, baselineAt, deployTimes) {
|
|
|
19167
19341
|
return changedAt > (baselineSha && deployTimes.get(baselineSha) || baselineAt) ? changedAt : null;
|
|
19168
19342
|
}
|
|
19169
19343
|
function computeRerun(input) {
|
|
19170
|
-
const { specs, ledger, log, touchIndex, drift, locks, attestations, now } = input;
|
|
19344
|
+
const { specs, ledger, log, touchIndex, drift, locks, attestations, dismissals, now } = input;
|
|
19171
19345
|
const range = buildRange(log, touchIndex);
|
|
19172
19346
|
const deployTimes = deployedAt(log);
|
|
19173
19347
|
const out = {};
|
|
@@ -19180,6 +19354,9 @@ function computeRerun(input) {
|
|
|
19180
19354
|
let audit = auditState(drift, spec.key, range);
|
|
19181
19355
|
let execution = executionState(coords, (sha) => freshness(sha, spec.key, range));
|
|
19182
19356
|
const driftEntry = drift.specs[spec.key];
|
|
19357
|
+
const dismissal = dismissals.specs[spec.key];
|
|
19358
|
+
const dismissed = dismissal !== void 0 && driftEntry !== void 0 && dismissal.auditRunId === driftEntry.runId && dismissal.label === driftEntry.label && (audit.audit === "drifted" || audit.audit === "undecided");
|
|
19359
|
+
if (dismissed) audit = { audit: "clean" };
|
|
19183
19360
|
const auditMoved = specMovedSince(spec.changedAt, driftEntry?.gitHead ?? null, driftEntry?.at ?? "", deployTimes);
|
|
19184
19361
|
const runMoved = specMovedSince(spec.changedAt, coords.lastRun?.deployedSha ?? null, coords.lastRun?.at ?? "", deployTimes);
|
|
19185
19362
|
if (auditMoved && audit.audit !== "due") audit = { audit: "due" };
|
|
@@ -19193,6 +19370,10 @@ function computeRerun(input) {
|
|
|
19193
19370
|
...auditMoved || runMoved ? { specChangedSince: auditMoved ?? runMoved } : {},
|
|
19194
19371
|
...audit,
|
|
19195
19372
|
...execution,
|
|
19373
|
+
...dismissal ? {
|
|
19374
|
+
auditDismissed: dismissal,
|
|
19375
|
+
auditDismissalApplied: dismissed
|
|
19376
|
+
} : {},
|
|
19196
19377
|
...manualState?.kind === "covers" ? { manual: manualState.attest } : {},
|
|
19197
19378
|
...manualState?.kind === "lapsed" ? {
|
|
19198
19379
|
manualLapsed: {
|
|
@@ -19356,14 +19537,15 @@ function createGetRerunHandler(storage) {
|
|
|
19356
19537
|
return async (ctx) => {
|
|
19357
19538
|
const project = requireSafeSegment(ctx.params.project, "project");
|
|
19358
19539
|
const profile = requireProfileParam(ctx.url);
|
|
19359
|
-
const [specs, ledger, log, touchIndex, drift, locks, attestations] = await Promise.all([
|
|
19540
|
+
const [specs, ledger, log, touchIndex, drift, locks, attestations, dismissals] = await Promise.all([
|
|
19360
19541
|
requireSpecTargets(storage.perspectives, project, "which specs need a re-run"),
|
|
19361
19542
|
storage.ledger.getMerged(project, profile),
|
|
19362
19543
|
storage.deploys.getLog(project, profile),
|
|
19363
19544
|
storage.deploys.getTouchIndex(project, profile),
|
|
19364
19545
|
storage.driftLedger.getMerged(project),
|
|
19365
19546
|
storage.locks.get(project, profile),
|
|
19366
|
-
storage.attestations.get(project, profile)
|
|
19547
|
+
storage.attestations.get(project, profile),
|
|
19548
|
+
storage.auditDismissals.get(project)
|
|
19367
19549
|
]);
|
|
19368
19550
|
const head = log.entries[log.entries.length - 1];
|
|
19369
19551
|
sendJson(ctx.res, 200, {
|
|
@@ -19378,6 +19560,7 @@ function createGetRerunHandler(storage) {
|
|
|
19378
19560
|
drift,
|
|
19379
19561
|
locks,
|
|
19380
19562
|
attestations,
|
|
19563
|
+
dismissals,
|
|
19381
19564
|
now: /* @__PURE__ */ new Date()
|
|
19382
19565
|
})
|
|
19383
19566
|
});
|
|
@@ -20921,7 +21104,7 @@ const CSS = `
|
|
|
20921
21104
|
.d-grid dt { color: var(--muted); font-size: 12px; padding-top: 1px; }
|
|
20922
21105
|
.d-grid dd { color: var(--fg-dim); }
|
|
20923
21106
|
.d-grid dd ul { list-style: none; display: flex; flex-direction: column; gap: 3px; margin: 0; padding: 0; }
|
|
20924
|
-
.d-grid dd li::before { content: "\\2022 "; color: var(--muted-2); }
|
|
21107
|
+
.d-grid dd ul li::before { content: "\\2022 "; color: var(--muted-2); }
|
|
20925
21108
|
.d-grid code { font-size: 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
|
|
20926
21109
|
/* Prose gets a measure so it stops wrapping mid-phrase in a narrow column;
|
|
20927
21110
|
paths wrap as whole chips, never inside a path. */
|
|
@@ -20930,17 +21113,26 @@ const CSS = `
|
|
|
20930
21113
|
.d-paths code { white-space: nowrap; }
|
|
20931
21114
|
.d-prose + .d-paths { margin-top: 6px; }
|
|
20932
21115
|
.manual-attest { margin-top: 14px; }
|
|
20933
|
-
.steps-box { margin-top: 14px; max-width: 900px; }
|
|
20934
|
-
.steps-box .slabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
20935
21116
|
.d-steps { margin: 4px 0 0; padding-left: 18px; display: flex; flex-direction: column; gap: 10px; font-size: 13px; color: var(--fg-dim); white-space: pre-line; }
|
|
20936
21117
|
.d-steps .step-expected { display: block; margin-top: 2px; font-size: 12.5px; }
|
|
20937
|
-
.notebox { margin-top: 14px; max-width: 900px; }
|
|
20938
|
-
.notebox .nlabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
20939
21118
|
.notebox textarea { width: 100%; min-height: 54px; resize: vertical; font: inherit; font-size: 13px; color: var(--fg-dim); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 8px 10px; }
|
|
20940
|
-
.notebox .nact { margin-top: 6px; display: flex; align-items: center; gap: 8px; }
|
|
21119
|
+
.notebox .nact { margin-top: 6px; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
20941
21120
|
.notebox .nstatus { font-size: 12px; color: var(--muted); }
|
|
20942
21121
|
.notebox .nstatus.ok { color: var(--pass); }
|
|
20943
21122
|
.notebox .nstatus.err { color: var(--fail); }
|
|
21123
|
+
/* ── detail panel: one card stack — reason, contents, note ── */
|
|
21124
|
+
.c-title .c-id { display: block; font-family: var(--mono); font-size: 11px; color: var(--muted-2); margin-top: 2px; }
|
|
21125
|
+
.p-sect { margin-top: 16px; max-width: 900px; }
|
|
21126
|
+
.p-sect:first-child { margin-top: 12px; }
|
|
21127
|
+
.p-slabel { font-size: 13px; font-weight: 600; color: var(--fg); margin-bottom: 6px; }
|
|
21128
|
+
/* The one-line state note beside the finding chip: what happens next. */
|
|
21129
|
+
.p-head-note { margin-left: auto; font-size: 12.5px; color: var(--muted); }
|
|
21130
|
+
/* The inline form an audit-dismissal or environment-attestation button
|
|
21131
|
+
expands into, in place of the two window.prompt() calls this replaces. */
|
|
21132
|
+
.override-form { margin-top: 10px; max-width: 480px; display: flex; flex-direction: column; gap: 10px; }
|
|
21133
|
+
.override-form textarea { width: 100%; min-height: 54px; resize: vertical; font: inherit; font-size: 13px; color: var(--fg-dim); background: var(--surface); border: 1px solid var(--border-strong); border-radius: var(--radius-sm); padding: 8px 10px; }
|
|
21134
|
+
.override-form .of-act { display: flex; align-items: center; gap: 8px; }
|
|
21135
|
+
.override-form .of-hint { font-size: 12px; color: var(--muted); max-width: 62ch; line-height: 1.5; }
|
|
20944
21136
|
|
|
20945
21137
|
@media (max-width: 900px) { .app { grid-template-columns: 1fr; } .sidebar { display: none; } .logo .wm { display: none; } .split { grid-template-columns: 1fr; } .rd-head .meta { margin-left: 0; } }
|
|
20946
21138
|
@media (max-width: 700px) { .prompt-grid { grid-template-columns: 1fr; } .prompt-diff { grid-template-columns: 1fr; } }
|
|
@@ -21062,16 +21254,15 @@ const CLIENT_JS = `
|
|
|
21062
21254
|
"perspectives.mode.deterministic": "deterministic", "perspectives.mode.live": "live",
|
|
21063
21255
|
"perspectives.ov.cases": "cases", "perspectives.ov.features": "features",
|
|
21064
21256
|
"perspectives.d.preconditions": "Preconditions", "perspectives.d.startScreen": "Start screen",
|
|
21065
|
-
"perspectives.d.testCondition": "Condition", "perspectives.d.
|
|
21066
|
-
"perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
|
|
21257
|
+
"perspectives.d.testCondition": "Condition", "perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
|
|
21067
21258
|
"perspectives.d.stepExpected": "Expected:",
|
|
21068
21259
|
"perspectives.note.label": "Note",
|
|
21069
21260
|
"perspectives.note.placeholder": "Notes about this case…",
|
|
21070
21261
|
"perspectives.note.saved": "Saved",
|
|
21071
21262
|
"perspectives.note.error": "Could not save — retry",
|
|
21072
|
-
"perspectives.d.lastRed": "Most recent failure",
|
|
21073
|
-
"perspectives.d.changedSince": "Changes since the last run",
|
|
21074
21263
|
"perspectives.d.whyVerdict": "Why this verdict",
|
|
21264
|
+
"perspectives.d.contents": "What this case does",
|
|
21265
|
+
"perspectives.finding.loadFailed": "Could not load the finding's detail \u2014 open the run to read it",
|
|
21075
21266
|
"perspectives.result.openRun": "Open this run in the hub",
|
|
21076
21267
|
"perspectives.result.ci": "CI",
|
|
21077
21268
|
"perspectives.rerun.state.needsRepair": "Needs repair",
|
|
@@ -21082,26 +21273,26 @@ const CLIENT_JS = `
|
|
|
21082
21273
|
"perspectives.rerun.vsDeploy": "judged against deploy",
|
|
21083
21274
|
"perspectives.rerun.noDeployHead": "no deploy recorded for this profile",
|
|
21084
21275
|
"perspectives.rerun.changedByDeploy": "deploy {sha} changed files matched to this case",
|
|
21085
|
-
"perspectives.rerun.changesSome": "
|
|
21086
|
-
"perspectives.rerun.changesNone": "
|
|
21276
|
+
"perspectives.rerun.changesSome": "changes matched to this case since the last run (as of deploy {sha})",
|
|
21277
|
+
"perspectives.rerun.changesNone": "no changes matched to this case since the last run (as of deploy {sha})",
|
|
21087
21278
|
"perspectives.rerun.touchedCount": "{n} deployed path(s) matched this case",
|
|
21088
21279
|
"perspectives.rerun.touchedUnknown": "a deploy since the last run matched this case",
|
|
21089
21280
|
"perspectives.rerun.inProgressHint": "an audit or a run is still going, or the audit has not caught up with the deploy",
|
|
21090
|
-
"perspectives.rerun.heldHint": "
|
|
21091
|
-
"perspectives.rerun.repair.testDrift": "the generated test
|
|
21092
|
-
"perspectives.rerun.repair.specChange": "the
|
|
21093
|
-
"perspectives.rerun.repair.auditUndecided": "the audit
|
|
21094
|
-
"perspectives.rerun.repair.runFailed": "the
|
|
21095
|
-
"perspectives.rerun.why.noSelectionInRange": "a deploy in range
|
|
21096
|
-
"perspectives.rerun.why.selectionUnknown": "the
|
|
21281
|
+
"perspectives.rerun.heldHint": "an audit, auto-fix or run job is working on this case — dismissing or attesting is unavailable until it finishes",
|
|
21282
|
+
"perspectives.rerun.repair.testDrift": "the audit judged the generated test code older than the implementation — auto-fix re-records it, so nothing is needed yet",
|
|
21283
|
+
"perspectives.rerun.repair.specChange": "the audit judged that the behaviour this test assumes is gone from the code — fix or delete the test, or dismiss the finding via “{dismissButton}” if it is wrong",
|
|
21284
|
+
"perspectives.rerun.repair.auditUndecided": "the audit could not tell whether the test has drifted — review the finding, then fix the test or dismiss it via “{dismissButton}”",
|
|
21285
|
+
"perspectives.rerun.repair.runFailed": "the latest run failed — this verdict will not change until the failure's cause is addressed",
|
|
21286
|
+
"perspectives.rerun.why.noSelectionInRange": "a deploy in range carries no impact judgement, so this case runs to be safe",
|
|
21287
|
+
"perspectives.rerun.why.selectionUnknown": "the latest deploy could not be judged as affecting this case or not, so it runs to be safe",
|
|
21097
21288
|
"perspectives.rerun.why.noDeployLog": "no deploy log for this profile",
|
|
21098
21289
|
"perspectives.rerun.why.unknownDeployedSha": "the last run's deployed commit is unknown",
|
|
21099
21290
|
"perspectives.rerun.why.ambiguousDeployedSha": "a deploy landed while the last run was executing",
|
|
21100
21291
|
"perspectives.rerun.why.deployedShaNotInLog": "the last run's commit predates the retained deploy log",
|
|
21101
21292
|
"perspectives.rerun.why.gapInRange": "deploys are missing from the range",
|
|
21102
21293
|
"perspectives.rerun.why.unrecognized": "this hub reported a reason this UI does not recognise",
|
|
21103
|
-
"perspectives.rerun.fix.noSelectionInRange": "A deploy
|
|
21104
|
-
"perspectives.rerun.fix.selectionUnknown": "A deploy
|
|
21294
|
+
"perspectives.rerun.fix.noSelectionInRange": "A deploy was recorded without an impact judgement, so whether it affected this case is unknown. This happens when the judgement did not finish in time, or was disabled, at record. The next run and audit retake the result.",
|
|
21295
|
+
"perspectives.rerun.fix.selectionUnknown": "A deploy could not be judged as affecting this case or not, so the next run retakes the result.",
|
|
21105
21296
|
"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.",
|
|
21106
21297
|
"perspectives.rerun.fix.unknownDeployedSha": "The last run did not record which commit the environment was running, so it cannot be positioned in the deploy log. Runs record it once this profile has a deploy log.",
|
|
21107
21298
|
"perspectives.rerun.fix.ambiguousDeployedSha": "A deploy landed while the last run was executing, so which commit it exercised is not knowable. Re-run this case to get a clean baseline.",
|
|
@@ -21113,10 +21304,8 @@ const CLIENT_JS = `
|
|
|
21113
21304
|
"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.",
|
|
21114
21305
|
"perspectives.rerun.deployHead": "deploy head",
|
|
21115
21306
|
"perspectives.drift.graded": "confirmed",
|
|
21116
|
-
"perspectives.manual.attestButton": "Mark as manually verified",
|
|
21117
21307
|
"perspectives.manual.revokeButton": "Revoke manual verification",
|
|
21118
|
-
"perspectives.manual.
|
|
21119
|
-
"perspectives.manual.promptNote": "Note (optional)",
|
|
21308
|
+
"perspectives.manual.envButton": "If the environment issue is resolved, use this",
|
|
21120
21309
|
"perspectives.manual.confirmRevoke": "Revoke the manual verification for this case?",
|
|
21121
21310
|
"perspectives.manual.error": "Could not save — retry",
|
|
21122
21311
|
"perspectives.manual.verifiedBy": "{by} manually verified this ({at})",
|
|
@@ -21126,6 +21315,18 @@ const CLIENT_JS = `
|
|
|
21126
21315
|
"perspectives.manual.lapsed.specEdited": "the manual verification lapsed when the spec was edited",
|
|
21127
21316
|
"perspectives.manual.lapsed.newerRed": "the manual verification lapsed after a later run failed",
|
|
21128
21317
|
"perspectives.manual.lapsed.unrecognized": "the manual verification lapsed for a reason this UI does not recognise",
|
|
21318
|
+
"perspectives.dismiss.offerButton": "If the test spec was fine, use this",
|
|
21319
|
+
"perspectives.dismiss.revokeButton": "Undo the dismissal",
|
|
21320
|
+
"perspectives.dismiss.confirmRevoke": "Undo the dismissal for this case?",
|
|
21321
|
+
"perspectives.dismiss.activeNote": "Audit finding “{headline}” was dismissed by {by} as a false positive ({at}) — “{note}”. The next run will settle it.",
|
|
21322
|
+
"perspectives.dismiss.priorNote": "This finding was previously dismissed by {by} ({at}) — “{note}”.",
|
|
21323
|
+
"perspectives.override.byLabel": "Verified by",
|
|
21324
|
+
"perspectives.override.reasonLabel": "Reason (required)",
|
|
21325
|
+
"perspectives.override.noteLabel": "What was resolved, and how you checked (required)",
|
|
21326
|
+
"perspectives.override.submit": "Record",
|
|
21327
|
+
"perspectives.override.cancel": "Never mind",
|
|
21328
|
+
"perspectives.override.dismissHint": "Withdraws the audit finding and puts this case back among the runnable. The verdict moves to “re-run needed”, and the next run's result settles it.",
|
|
21329
|
+
"perspectives.override.envHint": "The failure stays on record; only the verdict becomes “manually verified”. It lapses once a deploy reaches this case, and normal runs resume.",
|
|
21129
21330
|
"prompt.card.record": "Recording browser actions",
|
|
21130
21331
|
"prompt.card.live": "Live run (AI-driven)",
|
|
21131
21332
|
"prompt.card.playwright": "Playwright test generation",
|
|
@@ -21242,16 +21443,15 @@ const CLIENT_JS = `
|
|
|
21242
21443
|
"perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
|
|
21243
21444
|
"perspectives.ov.cases": "ケース", "perspectives.ov.features": "機能",
|
|
21244
21445
|
"perspectives.d.preconditions": "前提条件", "perspectives.d.startScreen": "開始画面",
|
|
21245
|
-
"perspectives.d.testCondition": "実行条件", "perspectives.d.
|
|
21246
|
-
"perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
|
|
21446
|
+
"perspectives.d.testCondition": "実行条件", "perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
|
|
21247
21447
|
"perspectives.d.stepExpected": "期待結果:",
|
|
21248
|
-
"perspectives.note.label": "
|
|
21448
|
+
"perspectives.note.label": "メモ",
|
|
21249
21449
|
"perspectives.note.placeholder": "このケースについてのメモ…",
|
|
21250
21450
|
"perspectives.note.saved": "保存しました",
|
|
21251
21451
|
"perspectives.note.error": "保存に失敗しました — 再試行してください",
|
|
21252
|
-
"perspectives.d.lastRed": "直近の失敗",
|
|
21253
|
-
"perspectives.d.changedSince": "前回実行以降の変更",
|
|
21254
21452
|
"perspectives.d.whyVerdict": "この判定の理由",
|
|
21453
|
+
"perspectives.d.contents": "テストの内容",
|
|
21454
|
+
"perspectives.finding.loadFailed": "詳細を読み込めませんでした。実行のページで確認してください",
|
|
21255
21455
|
"perspectives.result.openRun": "ハブでこの実行を開く",
|
|
21256
21456
|
"perspectives.result.ci": "CI",
|
|
21257
21457
|
"perspectives.rerun.state.needsRepair": "修正待ち",
|
|
@@ -21261,27 +21461,27 @@ const CLIENT_JS = `
|
|
|
21261
21461
|
"perspectives.rerun.state.verified": "検証済み",
|
|
21262
21462
|
"perspectives.rerun.vsDeploy": "判定基準: デプロイ",
|
|
21263
21463
|
"perspectives.rerun.noDeployHead": "このプロファイルにはデプロイの記録がありません",
|
|
21264
|
-
"perspectives.rerun.changedByDeploy": "デプロイ {sha}
|
|
21265
|
-
"perspectives.rerun.changesSome": "
|
|
21266
|
-
"perspectives.rerun.changesNone": "
|
|
21267
|
-
"perspectives.rerun.touchedCount": "
|
|
21268
|
-
"perspectives.rerun.touchedUnknown": "
|
|
21269
|
-
"perspectives.rerun.inProgressHint": "
|
|
21270
|
-
"perspectives.rerun.heldHint": "
|
|
21271
|
-
"perspectives.rerun.repair.testDrift": "
|
|
21272
|
-
"perspectives.rerun.repair.specChange": "
|
|
21273
|
-
"perspectives.rerun.repair.auditUndecided": "
|
|
21274
|
-
"perspectives.rerun.repair.runFailed": "
|
|
21275
|
-
"perspectives.rerun.why.noSelectionInRange": "
|
|
21276
|
-
"perspectives.rerun.why.selectionUnknown": "
|
|
21464
|
+
"perspectives.rerun.changedByDeploy": "デプロイ {sha} が、このケースに関係するファイルを変更しています",
|
|
21465
|
+
"perspectives.rerun.changesSome": "前回の実行より後に、このケースに関係する変更があります(デプロイ {sha} 時点)",
|
|
21466
|
+
"perspectives.rerun.changesNone": "前回の実行より後に、このケースに関係する変更はありません(デプロイ {sha} 時点)",
|
|
21467
|
+
"perspectives.rerun.touchedCount": "前回の実行より後のデプロイが、このケースに関係する変更を {n} 件含んでいます",
|
|
21468
|
+
"perspectives.rerun.touchedUnknown": "前回の実行より後のデプロイが、このケースに関係する変更を含んでいます",
|
|
21469
|
+
"perspectives.rerun.inProgressHint": "監査または実行のジョブが作業中か、最新デプロイに対する監査がまだ走っていません",
|
|
21470
|
+
"perspectives.rerun.heldHint": "監査・自動修正・実行のいずれかのジョブが、このケースを処理中です。終わるまで、このケースへの操作(棄却・手動確認)はできません",
|
|
21471
|
+
"perspectives.rerun.repair.testDrift": "監査が、生成済みのテストコードは実装より古いと判定しました。自動修正が録り直すので、まず対応は不要です",
|
|
21472
|
+
"perspectives.rerun.repair.specChange": "監査が、このテストの前提とする振る舞いは実装から無くなったと判定しました。テストを直すか削除するかを決めてください。指摘のほうが誤りなら、「{dismissButton}」から棄却できます",
|
|
21473
|
+
"perspectives.rerun.repair.auditUndecided": "テストが実装とズレているかどうか、監査では判断できませんでした。指摘の内容を確認して、テストを直すか、「{dismissButton}」から棄却してください",
|
|
21474
|
+
"perspectives.rerun.repair.runFailed": "直近の実行が失敗しています。失敗の原因に対処するまで、この判定は変わりません",
|
|
21475
|
+
"perspectives.rerun.why.noSelectionInRange": "影響判定なしで記録されたデプロイがあるため、念のため実行対象になっています",
|
|
21476
|
+
"perspectives.rerun.why.selectionUnknown": "直近のデプロイがこのケースに影響するかどうか判断できなかったため、念のため実行対象になっています",
|
|
21277
21477
|
"perspectives.rerun.why.noDeployLog": "このプロファイルのデプロイ記録がありません",
|
|
21278
21478
|
"perspectives.rerun.why.unknownDeployedSha": "前回実行時にデプロイされていたcommitが不明です",
|
|
21279
21479
|
"perspectives.rerun.why.ambiguousDeployedSha": "前回実行の途中でデプロイが発生しました",
|
|
21280
|
-
"perspectives.rerun.why.deployedShaNotInLog": "
|
|
21480
|
+
"perspectives.rerun.why.deployedShaNotInLog": "前回の実行が古く、どのデプロイに対して実行した結果か特定できません",
|
|
21281
21481
|
"perspectives.rerun.why.gapInRange": "対象範囲のデプロイ記録が欠けています",
|
|
21282
21482
|
"perspectives.rerun.why.unrecognized": "このUIが認識できない理由がハブから返されました",
|
|
21283
|
-
"perspectives.rerun.fix.noSelectionInRange": "
|
|
21284
|
-
"perspectives.rerun.fix.selectionUnknown": "
|
|
21483
|
+
"perspectives.rerun.fix.noSelectionInRange": "影響判定なしで記録されたデプロイがあり、このケースに影響したかどうか分かりません。デプロイの記録時に影響判定が時間内に終わらなかったか、無効化されていたときに起きます。次の実行と監査で結果を取り直します。",
|
|
21484
|
+
"perspectives.rerun.fix.selectionUnknown": "デプロイがこのケースに影響するかどうか判断できなかったため、次の実行で結果を取り直します。",
|
|
21285
21485
|
"perspectives.rerun.fix.noDeployLog": "このプロファイルのデプロイログに記録がありません。何がデプロイされたかをccqaに伝えるため、この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
21286
21486
|
"perspectives.rerun.fix.unknownDeployedSha": "前回実行は環境で動いていたcommitを記録していないため、デプロイログ上の位置を決められません。このプロファイルにデプロイログができれば、以降の実行では記録されます。",
|
|
21287
21487
|
"perspectives.rerun.fix.ambiguousDeployedSha": "前回実行の途中でデプロイが発生したため、どのcommitを検証したのか確定できません。基準を取り直すには再実行してください。",
|
|
@@ -21293,19 +21493,29 @@ const CLIENT_JS = `
|
|
|
21293
21493
|
"perspectives.rerun.noDeployLogBanner": "プロファイル {profile} にデプロイの記録がないため、どのケースも判定できません。この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
|
|
21294
21494
|
"perspectives.rerun.deployHead": "最新デプロイ",
|
|
21295
21495
|
"perspectives.drift.graded": "人が確認",
|
|
21296
|
-
"perspectives.manual.attestButton": "手動で確認した",
|
|
21297
21496
|
"perspectives.manual.revokeButton": "手動確認を取り消す",
|
|
21298
|
-
"perspectives.manual.
|
|
21299
|
-
"perspectives.manual.promptNote": "メモ(任意)",
|
|
21497
|
+
"perspectives.manual.envButton": "環境要因が解消した場合はこちら",
|
|
21300
21498
|
"perspectives.manual.confirmRevoke": "このケースの手動確認を取り消しますか?",
|
|
21301
21499
|
"perspectives.manual.error": "保存に失敗しました — 再試行してください",
|
|
21302
|
-
"perspectives.manual.verifiedBy": "{by}
|
|
21303
|
-
"perspectives.manual.lapsed.deployReached": "
|
|
21304
|
-
"perspectives.manual.lapsed.deployReachedNamed": "
|
|
21305
|
-
"perspectives.manual.lapsed.cannotPlace": "
|
|
21306
|
-
"perspectives.manual.lapsed.specEdited": "
|
|
21307
|
-
"perspectives.manual.lapsed.newerRed": "
|
|
21308
|
-
"perspectives.manual.lapsed.unrecognized": "このUI
|
|
21500
|
+
"perspectives.manual.verifiedBy": "{by} さんが手動で動作確認しました({at})",
|
|
21501
|
+
"perspectives.manual.lapsed.deployReached": "デプロイがこのケースに届いたため、手動確認は失効しました",
|
|
21502
|
+
"perspectives.manual.lapsed.deployReachedNamed": "デプロイ {sha}({at})がこのケースに届いたため、手動確認は失効しました",
|
|
21503
|
+
"perspectives.manual.lapsed.cannotPlace": "基準のデプロイをログで特定できなくなったため、手動確認は失効しました",
|
|
21504
|
+
"perspectives.manual.lapsed.specEdited": "spec が編集されたため、手動確認は失効しました",
|
|
21505
|
+
"perspectives.manual.lapsed.newerRed": "手動確認より後の実行が失敗したため、手動確認は失効しました",
|
|
21506
|
+
"perspectives.manual.lapsed.unrecognized": "この UI が認識できない理由により、手動確認は失効しました",
|
|
21507
|
+
"perspectives.dismiss.offerButton": "テスト仕様に問題がなかった場合はこちら",
|
|
21508
|
+
"perspectives.dismiss.revokeButton": "棄却を取り消す",
|
|
21509
|
+
"perspectives.dismiss.confirmRevoke": "このケースの棄却を取り消しますか?",
|
|
21510
|
+
"perspectives.dismiss.activeNote": "{by} さんが監査指摘「{headline}」を誤検知として棄却しました({at}・理由: {note})。次の実行結果が正否を決めます。",
|
|
21511
|
+
"perspectives.dismiss.priorNote": "この指摘は、以前 {by} さんが棄却しています({at}・理由: {note})",
|
|
21512
|
+
"perspectives.override.byLabel": "確認した人",
|
|
21513
|
+
"perspectives.override.reasonLabel": "理由(必須)",
|
|
21514
|
+
"perspectives.override.noteLabel": "解消と確認の内容(必須)",
|
|
21515
|
+
"perspectives.override.submit": "記録する",
|
|
21516
|
+
"perspectives.override.cancel": "やめる",
|
|
21517
|
+
"perspectives.override.dismissHint": "監査の指摘を取り下げて、このケースを実行対象に戻します。判定は「要再実行」になり、次の実行結果が正否を決めます。",
|
|
21518
|
+
"perspectives.override.envHint": "失敗の記録は残したまま、判定だけが「手動確認済み」になります。次のデプロイがこのケースに届くと失効し、通常の実行に戻ります。",
|
|
21309
21519
|
"prompt.card.record": "ブラウザ操作の記録",
|
|
21310
21520
|
"prompt.card.live": "ライブ実行(AI操作)",
|
|
21311
21521
|
"prompt.card.playwright": "Playwrightテスト生成",
|
|
@@ -21602,7 +21812,6 @@ const CLIENT_JS = `
|
|
|
21602
21812
|
return span;
|
|
21603
21813
|
}
|
|
21604
21814
|
|
|
21605
|
-
|
|
21606
21815
|
// Shared by the runs-list row and the run-detail header — the one place
|
|
21607
21816
|
// both decide whether a run's own status badge speaks drift's vocabulary.
|
|
21608
21817
|
function runStatusBadge(run) {
|
|
@@ -22212,15 +22421,13 @@ const CLIENT_JS = `
|
|
|
22212
22421
|
|
|
22213
22422
|
// ── run detail: spec cards ──────────────────────────────────────────
|
|
22214
22423
|
|
|
22215
|
-
// The diagnosis card
|
|
22216
|
-
//
|
|
22217
|
-
//
|
|
22218
|
-
//
|
|
22219
|
-
//
|
|
22220
|
-
//
|
|
22221
|
-
function
|
|
22222
|
-
var wrap = el("div", "analysis-box");
|
|
22223
|
-
var a = r.analysis;
|
|
22424
|
+
// The diagnosis card's two halves, shared by the run view (analysisSection)
|
|
22425
|
+
// and the perspectives reason card so the two renderings cannot drift:
|
|
22426
|
+
// verdict head (label chip + confidence), then the cause→fix pair as
|
|
22427
|
+
// labelled rows — headline and recommendation are one causal unit, so they
|
|
22428
|
+
// read as one. subDiagnosis is deliberately NOT shown: it is a machine
|
|
22429
|
+
// vocabulary for accuracy stratification and learning, not for humans.
|
|
22430
|
+
function diagnosisHead(a) {
|
|
22224
22431
|
var head = el("div", "analysis-head");
|
|
22225
22432
|
head.appendChild(labelChip(a.label));
|
|
22226
22433
|
// Which repair a SPEC_CHANGE needs — delete the spec, or rewrite it. The
|
|
@@ -22230,7 +22437,10 @@ const CLIENT_JS = `
|
|
|
22230
22437
|
head.appendChild(el("span", "chip spec-change-chip", t("diag.specChangeKind." + a.specChangeKind)));
|
|
22231
22438
|
}
|
|
22232
22439
|
head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
|
|
22233
|
-
|
|
22440
|
+
return head;
|
|
22441
|
+
}
|
|
22442
|
+
|
|
22443
|
+
function diagnosisKv(a) {
|
|
22234
22444
|
var kv = el("div", "analysis-kv");
|
|
22235
22445
|
// Set only when the verdict blames the test case (TEST_DRIFT/SPEC_CHANGE),
|
|
22236
22446
|
// on both kinds of row — it names the half that has to be repaired.
|
|
@@ -22246,7 +22456,14 @@ const CLIENT_JS = `
|
|
|
22246
22456
|
kv.appendChild(el("div", "k", t("diag.fix")));
|
|
22247
22457
|
kv.appendChild(el("div", "v", a.recommendation));
|
|
22248
22458
|
}
|
|
22249
|
-
|
|
22459
|
+
return kv.childNodes.length > 0 ? kv : null;
|
|
22460
|
+
}
|
|
22461
|
+
|
|
22462
|
+
function analysisSection(runId, r) {
|
|
22463
|
+
var wrap = el("div", "analysis-box");
|
|
22464
|
+
wrap.appendChild(diagnosisHead(r.analysis));
|
|
22465
|
+
var kv = diagnosisKv(r.analysis);
|
|
22466
|
+
if (kv) wrap.appendChild(kv);
|
|
22250
22467
|
return wrap;
|
|
22251
22468
|
}
|
|
22252
22469
|
|
|
@@ -23359,9 +23576,10 @@ const CLIENT_JS = `
|
|
|
23359
23576
|
// this hub answers at all. Chip visibility follows it rather than the report,
|
|
23360
23577
|
// so switching profile doesn't drop the filter while the next one loads.
|
|
23361
23578
|
// "drift" is the DriftLedgerResponse, or null when unanswered (older hub, or
|
|
23362
|
-
// a failed fetch) — not profile-scoped
|
|
23363
|
-
//
|
|
23364
|
-
//
|
|
23579
|
+
// a failed fetch) — not profile-scoped. It backs the audit column's
|
|
23580
|
+
// "audited at" line and the reason card's finding (runId/label/headline),
|
|
23581
|
+
// so reloadRerun refetches it alongside the rerun report to keep the two
|
|
23582
|
+
// reports of one card equally fresh.
|
|
23365
23583
|
var perspState = {
|
|
23366
23584
|
doc: null, q: "", f: "all",
|
|
23367
23585
|
rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
|
|
@@ -23403,6 +23621,13 @@ const CLIENT_JS = `
|
|
|
23403
23621
|
"/attestations?profile=" + encodeURIComponent(state.profile);
|
|
23404
23622
|
}
|
|
23405
23623
|
|
|
23624
|
+
// A person's answer to an audit finding, not an environment: the finding is
|
|
23625
|
+
// about the repository, not a deployed profile, so this carries no
|
|
23626
|
+
// ?profile= (unlike attestationsPath above).
|
|
23627
|
+
function auditDismissalsPath() {
|
|
23628
|
+
return "/api/v1/projects/" + encodeURIComponent(state.project) + "/audit-dismissals";
|
|
23629
|
+
}
|
|
23630
|
+
|
|
23406
23631
|
// Resolves { report } or { note } and never rejects: a hub that predates
|
|
23407
23632
|
// the endpoint costs only the columns it feeds, not the whole tab. A 404
|
|
23408
23633
|
// here can only mean "no such route" — the endpoint's own 404 is "the
|
|
@@ -23422,10 +23647,10 @@ const CLIENT_JS = `
|
|
|
23422
23647
|
|
|
23423
23648
|
// ── perspectives: drift ledger ────────────────────────────────────────
|
|
23424
23649
|
// Not profile-scoped (see perspState.drift above), so unlike rerunPath this
|
|
23425
|
-
// takes no ?profile=.
|
|
23426
|
-
//
|
|
23427
|
-
//
|
|
23428
|
-
// column's evidence line
|
|
23650
|
+
// takes no ?profile=. The audit AXIS is answered by the /rerun report
|
|
23651
|
+
// (ADR-0014); this ledger supplies what that report does not carry — the
|
|
23652
|
+
// finding's own coordinate and words (runId/at/label/headline), shown in
|
|
23653
|
+
// the audit column's evidence line and the reason card.
|
|
23429
23654
|
|
|
23430
23655
|
function driftPath() {
|
|
23431
23656
|
return "/api/v1/projects/" + encodeURIComponent(state.project) + "/drift";
|
|
@@ -23478,7 +23703,10 @@ const CLIENT_JS = `
|
|
|
23478
23703
|
// when it has no entry).
|
|
23479
23704
|
function rerunReasonText(prefix, reason) {
|
|
23480
23705
|
var text = t(prefix + reason);
|
|
23481
|
-
|
|
23706
|
+
if (text === prefix + reason) text = t(prefix + "unrecognized");
|
|
23707
|
+
// Wordings that point at the dismiss control name it by its own label, so
|
|
23708
|
+
// renaming the button cannot silently strand four strings.
|
|
23709
|
+
return text.replace("{dismissButton}", t("perspectives.dismiss.offerButton"));
|
|
23482
23710
|
}
|
|
23483
23711
|
|
|
23484
23712
|
// Who attested and when, plus their note if they left one — the whole
|
|
@@ -23569,7 +23797,6 @@ const CLIENT_JS = `
|
|
|
23569
23797
|
return lapse ? why + " · " + lapse : why;
|
|
23570
23798
|
}
|
|
23571
23799
|
|
|
23572
|
-
|
|
23573
23800
|
// --- pure: rerun composition ---------------------------------------------
|
|
23574
23801
|
// Self-contained on purpose: no DOM, no closures. rerun-view.test.ts lifts
|
|
23575
23802
|
// this region out of the rendered page and runs it, because the summary bar
|
|
@@ -24011,8 +24238,8 @@ const CLIENT_JS = `
|
|
|
24011
24238
|
|
|
24012
24239
|
// --- pure: rerun detail labels -------------------------------------------
|
|
24013
24240
|
// Self-contained on purpose (no DOM, no closures) so rerun-view.test.ts can
|
|
24014
|
-
// lift this region out of the rendered page and run it:
|
|
24015
|
-
//
|
|
24241
|
+
// lift this region out of the rendered page and run it: whether the deploy
|
|
24242
|
+
// log answered for this case, and which deploy the reason line names.
|
|
24016
24243
|
|
|
24017
24244
|
// The deploy log answered for this case: the row can show what it holds.
|
|
24018
24245
|
// A case the log could not place has no evidence to show even though its
|
|
@@ -24022,20 +24249,6 @@ const CLIENT_JS = `
|
|
|
24022
24249
|
return rr.verdict === "rerunNeeded" && !rr.executionAssumedReached;
|
|
24023
24250
|
}
|
|
24024
24251
|
|
|
24025
|
-
// Evidence is labelled by the timeframe it covers; everything else names why
|
|
24026
|
-
// the verdict landed — a different kind of content, and forcing one label
|
|
24027
|
-
// over both would make one of the two read as a lie.
|
|
24028
|
-
function rerunEvidenceLabelKey(rr) {
|
|
24029
|
-
return rerunHasEvidence(rr) ? "perspectives.d.changedSince" : "perspectives.d.whyVerdict";
|
|
24030
|
-
}
|
|
24031
|
-
|
|
24032
|
-
// The failure row points at a run. With no failure there is nothing to point
|
|
24033
|
-
// at, so the row is omitted rather than filled with "never failed" — the row
|
|
24034
|
-
// above already carries the last result.
|
|
24035
|
-
function rerunHasFailure(rr) {
|
|
24036
|
-
return !!(rr && rr.lastRed);
|
|
24037
|
-
}
|
|
24038
|
-
|
|
24039
24252
|
// Which deploy the evidence line names, and how. A "needed" verdict carries
|
|
24040
24253
|
// the deploy that caused it (touchedByDeploy) when the hub could confirm one,
|
|
24041
24254
|
// and that is the deploy a reader wants — so it is named, with when it
|
|
@@ -24056,51 +24269,81 @@ const CLIENT_JS = `
|
|
|
24056
24269
|
}
|
|
24057
24270
|
// --- end pure: rerun detail labels ----------------------------------------
|
|
24058
24271
|
|
|
24059
|
-
//
|
|
24060
|
-
//
|
|
24061
|
-
//
|
|
24062
|
-
//
|
|
24063
|
-
|
|
24064
|
-
|
|
24065
|
-
|
|
24066
|
-
|
|
24067
|
-
|
|
24272
|
+
// --- pure: audit dismissal reading ----------------------------------------
|
|
24273
|
+
// Self-contained (no DOM, no closures) for the same reason as the regions
|
|
24274
|
+
// above: read rr.auditDismissed against rr.audit, per the schema's own
|
|
24275
|
+
// comment on the field. "clean" means the dismissal is what is holding the
|
|
24276
|
+
// axis there; "drifted"/"undecided" means a later audit re-raised what it
|
|
24277
|
+
// answered, so the old dismissal no longer covers it.
|
|
24278
|
+
// The audit has something outstanding on this spec. Both values mean the
|
|
24279
|
+
// same thing to a reader deciding whether to answer it: the audit read the
|
|
24280
|
+
// code and did not clear the spec (ADR-0019).
|
|
24281
|
+
function auditOpen(rr) {
|
|
24282
|
+
return !!rr && (rr.audit === "drifted" || rr.audit === "undecided");
|
|
24283
|
+
}
|
|
24284
|
+
function auditDismissalActive(rr) {
|
|
24285
|
+
// The hub says whether the dismissal settled the axis. Inferring it from
|
|
24286
|
+
// "clean" would credit the person for a later audit clearing the spec on
|
|
24287
|
+
// its own, which reads identically here.
|
|
24288
|
+
return !!(rr && rr.auditDismissed && rr.auditDismissalApplied);
|
|
24289
|
+
}
|
|
24290
|
+
function auditDismissalReflagged(rr) {
|
|
24291
|
+
return !!(rr && rr.auditDismissed && auditOpen(rr));
|
|
24292
|
+
}
|
|
24293
|
+
// --- end pure: audit dismissal reading -------------------------------------
|
|
24294
|
+
|
|
24295
|
+
// The dismissal's own words, read against the current audit state: active,
|
|
24296
|
+
// it explains why the axis reads clean; re-flagged, it is a fact worth
|
|
24297
|
+
// keeping visible beside the finding that reopened it.
|
|
24298
|
+
function rerunDismissalLine(rr) {
|
|
24299
|
+
if (!rr || !rr.auditDismissed) return null;
|
|
24300
|
+
var d = rr.auditDismissed;
|
|
24301
|
+
if (auditDismissalActive(rr)) {
|
|
24302
|
+
return {
|
|
24303
|
+
muted: false,
|
|
24304
|
+
text: t("perspectives.dismiss.activeNote")
|
|
24305
|
+
.replace("{headline}", d.headline).replace("{by}", d.by).replace("{at}", relTime(d.at)).replace("{note}", d.note),
|
|
24306
|
+
};
|
|
24068
24307
|
}
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
|
|
24072
|
-
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
wrap.appendChild(el("div", "d-prose", text));
|
|
24076
|
-
// A touch the index proved but cannot enumerate leaves no paths to list;
|
|
24077
|
-
// the line above still says a change landed, which is all that is known.
|
|
24078
|
-
if (rr.verdict === "rerunNeeded" && rr.touchedBy && rr.touchedBy.length) {
|
|
24079
|
-
wrap.appendChild(pathCodes(rr.touchedBy));
|
|
24308
|
+
if (auditDismissalReflagged(rr)) {
|
|
24309
|
+
return {
|
|
24310
|
+
muted: true,
|
|
24311
|
+
text: t("perspectives.dismiss.priorNote")
|
|
24312
|
+
.replace("{by}", d.by).replace("{at}", relTime(d.at)).replace("{note}", d.note),
|
|
24313
|
+
};
|
|
24080
24314
|
}
|
|
24081
|
-
return
|
|
24315
|
+
return null;
|
|
24082
24316
|
}
|
|
24083
24317
|
|
|
24084
|
-
// The
|
|
24085
|
-
//
|
|
24086
|
-
//
|
|
24087
|
-
//
|
|
24088
|
-
function
|
|
24318
|
+
// The reason card's body when no run-recorded finding is shown: what the
|
|
24319
|
+
// deploy log holds since this case last ran (rerunChangeLine), or why the
|
|
24320
|
+
// verdict landed. The dismissal and lapse notes are the card's own to
|
|
24321
|
+
// append (perspReasonCard), not this value's.
|
|
24322
|
+
function rerunEvidenceValue(rr) {
|
|
24089
24323
|
var wrap = el("div");
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
|
|
24093
|
-
|
|
24094
|
-
|
|
24324
|
+
if (!rerunHasEvidence(rr)) {
|
|
24325
|
+
wrap.appendChild(el("div", "d-prose", rerunWhyVerdict(rr)));
|
|
24326
|
+
} else {
|
|
24327
|
+
// Both states require a non-empty deploy log, so a head-less report
|
|
24328
|
+
// contradicts itself; rerunChangeLine then names what is missing rather
|
|
24329
|
+
// than inventing a baseline.
|
|
24330
|
+
var line = rerunChangeLine(rr, perspState.rerun && perspState.rerun.deployHead);
|
|
24331
|
+
var text = t(line.key).replace("{sha}", shortSha(line.sha));
|
|
24332
|
+
if (line.at) text += " · " + relTime(line.at);
|
|
24333
|
+
wrap.appendChild(el("div", "d-prose", text));
|
|
24334
|
+
// A touch the index proved but cannot enumerate leaves no paths to
|
|
24335
|
+
// list; the line above still says a change landed, which is all that
|
|
24336
|
+
// is known.
|
|
24337
|
+
if (rr.verdict === "rerunNeeded" && rr.touchedBy && rr.touchedBy.length) {
|
|
24338
|
+
wrap.appendChild(pathCodes(rr.touchedBy));
|
|
24339
|
+
}
|
|
24095
24340
|
}
|
|
24096
24341
|
return wrap;
|
|
24097
24342
|
}
|
|
24098
24343
|
|
|
24099
|
-
// Lets a person's own check stand in for the machine's verdict.
|
|
24100
|
-
//
|
|
24101
|
-
//
|
|
24102
|
-
// is the same profile-scoped refresh a profile switch uses — it re-renders
|
|
24103
|
-
// the whole table, so an open detail panel closes along with it.
|
|
24344
|
+
// Lets a person's own check stand in for the machine's verdict. reloadRerun()
|
|
24345
|
+
// is the same refresh a profile switch uses — it re-renders the whole
|
|
24346
|
+
// table, so an open detail panel closes along with it.
|
|
24104
24347
|
function submitAttestation(method, body) {
|
|
24105
24348
|
apiFetch(attestationsPath(), {
|
|
24106
24349
|
method: method,
|
|
@@ -24110,17 +24353,16 @@ const CLIENT_JS = `
|
|
|
24110
24353
|
.catch(function (err) { window.alert(t("perspectives.manual.error") + ": " + err.message); });
|
|
24111
24354
|
}
|
|
24112
24355
|
|
|
24113
|
-
|
|
24114
|
-
|
|
24115
|
-
|
|
24116
|
-
|
|
24117
|
-
|
|
24118
|
-
|
|
24119
|
-
|
|
24120
|
-
|
|
24121
|
-
|
|
24122
|
-
|
|
24123
|
-
return btn;
|
|
24356
|
+
// Lets a person say an audit finding was wrong. Same reload contract as
|
|
24357
|
+
// submitAttestation above; a different endpoint (no ?profile=, ADR: a
|
|
24358
|
+
// finding is about the repository).
|
|
24359
|
+
function submitAuditDismissal(method, body) {
|
|
24360
|
+
apiFetch(auditDismissalsPath(), {
|
|
24361
|
+
method: method,
|
|
24362
|
+
headers: { "Content-Type": "application/json" },
|
|
24363
|
+
body: JSON.stringify(body),
|
|
24364
|
+
}).then(function () { reloadRerun(); })
|
|
24365
|
+
.catch(function (err) { window.alert(t("perspectives.manual.error") + ": " + err.message); });
|
|
24124
24366
|
}
|
|
24125
24367
|
|
|
24126
24368
|
function manualRevokeButton(feature, spec) {
|
|
@@ -24133,16 +24375,251 @@ const CLIENT_JS = `
|
|
|
24133
24375
|
return btn;
|
|
24134
24376
|
}
|
|
24135
24377
|
|
|
24136
|
-
|
|
24137
|
-
|
|
24138
|
-
|
|
24139
|
-
|
|
24140
|
-
|
|
24141
|
-
|
|
24142
|
-
|
|
24143
|
-
|
|
24378
|
+
function auditDismissalRevokeButton(feature, spec) {
|
|
24379
|
+
var btn = el("button", "btn ghost sm del", t("perspectives.dismiss.revokeButton"));
|
|
24380
|
+
btn.type = "button";
|
|
24381
|
+
btn.addEventListener("click", function () {
|
|
24382
|
+
if (!window.confirm(t("perspectives.dismiss.confirmRevoke"))) return;
|
|
24383
|
+
submitAuditDismissal("DELETE", { spec: perspSpecKey(feature, spec) });
|
|
24384
|
+
});
|
|
24385
|
+
return btn;
|
|
24386
|
+
}
|
|
24387
|
+
|
|
24388
|
+
// The inline form a "dismiss" or "environment" offer button expands into,
|
|
24389
|
+
// in place of the two window.prompt() calls this replaces. Both kinds ask
|
|
24390
|
+
// for the same two things — who, and why — and differ only in wording and
|
|
24391
|
+
// which endpoint the answer goes to.
|
|
24392
|
+
function buildOverrideForm(kind, feature, spec, onCancel) {
|
|
24393
|
+
var wrap = el("div", "override-form");
|
|
24394
|
+
|
|
24395
|
+
var byRow = el("div", "form-row");
|
|
24396
|
+
byRow.appendChild(el("label", null, t("perspectives.override.byLabel")));
|
|
24397
|
+
var byInput = el("input", "input");
|
|
24398
|
+
byInput.type = "text";
|
|
24399
|
+
byInput.value = loadAttestBy();
|
|
24400
|
+
byRow.appendChild(byInput);
|
|
24401
|
+
wrap.appendChild(byRow);
|
|
24402
|
+
|
|
24403
|
+
var noteRow = el("div", "form-row");
|
|
24404
|
+
noteRow.appendChild(el("label", null, t(kind === "dismiss" ? "perspectives.override.reasonLabel" : "perspectives.override.noteLabel")));
|
|
24405
|
+
var noteInput = el("textarea");
|
|
24406
|
+
noteRow.appendChild(noteInput);
|
|
24407
|
+
wrap.appendChild(noteRow);
|
|
24408
|
+
|
|
24409
|
+
var act = el("div", "of-act");
|
|
24410
|
+
var submitBtn = el("button", "btn sm primary", t("perspectives.override.submit"));
|
|
24411
|
+
submitBtn.type = "button";
|
|
24412
|
+
submitBtn.disabled = true;
|
|
24413
|
+
var cancelBtn = el("button", "btn ghost sm", t("perspectives.override.cancel"));
|
|
24414
|
+
cancelBtn.type = "button";
|
|
24415
|
+
act.appendChild(submitBtn);
|
|
24416
|
+
act.appendChild(cancelBtn);
|
|
24417
|
+
wrap.appendChild(act);
|
|
24418
|
+
|
|
24419
|
+
wrap.appendChild(el("div", "of-hint", t(kind === "dismiss" ? "perspectives.override.dismissHint" : "perspectives.override.envHint")));
|
|
24420
|
+
|
|
24421
|
+
function syncEnabled() {
|
|
24422
|
+
submitBtn.disabled = !(byInput.value.trim() && noteInput.value.trim());
|
|
24423
|
+
}
|
|
24424
|
+
byInput.addEventListener("input", syncEnabled);
|
|
24425
|
+
noteInput.addEventListener("input", syncEnabled);
|
|
24426
|
+
cancelBtn.addEventListener("click", onCancel);
|
|
24427
|
+
|
|
24428
|
+
submitBtn.addEventListener("click", function () {
|
|
24429
|
+
var by = byInput.value.trim();
|
|
24430
|
+
var note = noteInput.value.trim();
|
|
24431
|
+
if (!by || !note) return;
|
|
24432
|
+
storeAttestBy(by);
|
|
24433
|
+
if (kind === "dismiss") submitAuditDismissal("PUT", { spec: perspSpecKey(feature, spec), by: by, note: note });
|
|
24434
|
+
else submitAttestation("PUT", { spec: perspSpecKey(feature, spec), by: by, note: note });
|
|
24435
|
+
});
|
|
24436
|
+
|
|
24437
|
+
return wrap;
|
|
24438
|
+
}
|
|
24439
|
+
|
|
24440
|
+
// A button that expands into buildOverrideForm above in place, rather than
|
|
24441
|
+
// a modal — the action is rare enough that swapping the button for its own
|
|
24442
|
+
// form reads fine without one.
|
|
24443
|
+
function buildOverrideOffer(kind, feature, spec) {
|
|
24444
|
+
var box = el("div", "manual-attest");
|
|
24445
|
+
var openBtn = el("button", "btn sm primary", t(kind === "dismiss" ? "perspectives.dismiss.offerButton" : "perspectives.manual.envButton"));
|
|
24446
|
+
openBtn.type = "button";
|
|
24447
|
+
box.appendChild(openBtn);
|
|
24448
|
+
openBtn.addEventListener("click", function () {
|
|
24449
|
+
box.removeChild(openBtn);
|
|
24450
|
+
var form = buildOverrideForm(kind, feature, spec, function () {
|
|
24451
|
+
box.removeChild(form);
|
|
24452
|
+
box.appendChild(openBtn);
|
|
24453
|
+
});
|
|
24454
|
+
box.appendChild(form);
|
|
24455
|
+
});
|
|
24456
|
+
return box;
|
|
24457
|
+
}
|
|
24458
|
+
|
|
24459
|
+
// The audit-axis override slot: dismiss an open finding, or revoke a
|
|
24460
|
+
// dismissal that is currently the reason the axis reads clean. At most one
|
|
24461
|
+
// of the two ever shows — a finding the axis
|
|
24462
|
+
// itself has cleared, dismissed or not, offers nothing here.
|
|
24463
|
+
// While a job holds the spec, no control shows at all: heldHint promises
|
|
24464
|
+
// the reader that overrides wait for the job, so the buttons must too.
|
|
24465
|
+
function auditOverrideBox(feature, spec, rr) {
|
|
24466
|
+
if (rr.heldBy) return null;
|
|
24467
|
+
if (auditDismissalActive(rr)) {
|
|
24468
|
+
var box = el("div", "manual-attest");
|
|
24469
|
+
box.appendChild(auditDismissalRevokeButton(feature, spec));
|
|
24470
|
+
return box;
|
|
24471
|
+
}
|
|
24472
|
+
if (auditOpen(rr)) return buildOverrideOffer("dismiss", feature, spec);
|
|
24473
|
+
return null;
|
|
24474
|
+
}
|
|
24475
|
+
|
|
24476
|
+
// The execution-axis override slot: revoke a standing attestation, or offer
|
|
24477
|
+
// one for an environment-caused failure — but only when the audit axis has
|
|
24478
|
+
// no open finding of its own, which is auditOverrideBox's problem to answer,
|
|
24479
|
+
// not this one's.
|
|
24480
|
+
function executionOverrideBox(feature, spec, rr) {
|
|
24481
|
+
if (rr.heldBy) return null;
|
|
24482
|
+
if (rr.manual) {
|
|
24483
|
+
var box = el("div", "manual-attest");
|
|
24484
|
+
if (rr.verdict !== "manuallyVerified") box.appendChild(el("div", "d-prose", manualAttestationText(rr.manual)));
|
|
24485
|
+
box.appendChild(manualRevokeButton(feature, spec));
|
|
24486
|
+
return box;
|
|
24487
|
+
}
|
|
24488
|
+
// Only once the audit has actually cleared the spec: while it is still
|
|
24489
|
+
// due, nobody knows yet whether the environment was the only thing wrong.
|
|
24490
|
+
if (rr.audit === "clean" && rr.execution === "failed" && rr.lastRed && rr.lastRed.label === "ENVIRONMENT") {
|
|
24491
|
+
return buildOverrideOffer("environment", feature, spec);
|
|
24492
|
+
}
|
|
24493
|
+
return null;
|
|
24494
|
+
}
|
|
24495
|
+
|
|
24496
|
+
// ── the reason card ──────────────────────────────────────────────────────
|
|
24497
|
+
// One card that answers "why is the verdict what it is". When an axis
|
|
24498
|
+
// stands on a run-recorded finding (an open audit finding, or the failure
|
|
24499
|
+
// the execution axis reports), the card carries that finding in full,
|
|
24500
|
+
// fetched from the run's own report — where cause, fix and evidence
|
|
24501
|
+
// already live. Everything a person may do about the state (dismiss,
|
|
24502
|
+
// attest, revoke) sits in the same card, beside the reason it answers.
|
|
24503
|
+
|
|
24504
|
+
var runReportCache = {};
|
|
24505
|
+
function fetchRunReport(runId) {
|
|
24506
|
+
if (!runReportCache[runId]) {
|
|
24507
|
+
// Same endpoint (and so the same HTTP cache entry) the run view reads.
|
|
24508
|
+
// A failure is not cached: a transient 502 costs one refetch on the
|
|
24509
|
+
// next expand instead of pinning the fallback for the session.
|
|
24510
|
+
runReportCache[runId] = apiFetch("/api/v1/runs/" + encodeURIComponent(runId) + "/report")
|
|
24511
|
+
.catch(function () { delete runReportCache[runId]; return null; });
|
|
24512
|
+
}
|
|
24513
|
+
return runReportCache[runId];
|
|
24514
|
+
}
|
|
24515
|
+
|
|
24516
|
+
function reportRowFor(report, key) {
|
|
24517
|
+
var rows = (report && report.results) || [];
|
|
24518
|
+
for (var i = 0; i < rows.length; i++) {
|
|
24519
|
+
if (rows[i].feature + "/" + rows[i].spec === key) return rows[i];
|
|
24520
|
+
}
|
|
24521
|
+
return null;
|
|
24522
|
+
}
|
|
24523
|
+
|
|
24524
|
+
// Which run-recorded finding the card shows, if any: an open audit finding
|
|
24525
|
+
// wins (it is why nothing runs), else the failure the execution axis stands
|
|
24526
|
+
// on. The ledger's own label/headline are the instant fallback while the
|
|
24527
|
+
// report loads — and the whole content if it never arrives.
|
|
24528
|
+
function reasonFindingSource(rr, driftEntry) {
|
|
24529
|
+
if (auditOpen(rr) && driftEntry && driftEntry.runId && driftEntry.label) return driftEntry;
|
|
24530
|
+
if (rr.execution === "failed" && rr.lastRed && rr.lastRed.runId && rr.lastRed.label) return rr.lastRed;
|
|
24531
|
+
return null;
|
|
24532
|
+
}
|
|
24533
|
+
|
|
24534
|
+
function perspReasonCard(feature, spec, rr, driftEntry) {
|
|
24535
|
+
var card = el("div", "analysis-box");
|
|
24536
|
+
var source = reasonFindingSource(rr, driftEntry);
|
|
24537
|
+
|
|
24538
|
+
if (source) {
|
|
24539
|
+
// The ledger's label/headline render at once; the run's own report
|
|
24540
|
+
// replaces them with the full diagnosis (the same head/kv the run view
|
|
24541
|
+
// builds) when — and if — it arrives.
|
|
24542
|
+
var note = el("span", "p-head-note", rerunWhyVerdict(rr));
|
|
24543
|
+
var slot = el("div");
|
|
24544
|
+
var head = el("div", "analysis-head");
|
|
24545
|
+
head.appendChild(labelChip(source.label));
|
|
24546
|
+
head.appendChild(note);
|
|
24547
|
+
slot.appendChild(head);
|
|
24548
|
+
if (source.headline) {
|
|
24549
|
+
var kv = diagnosisKv({ headline: source.headline });
|
|
24550
|
+
if (kv) slot.appendChild(kv);
|
|
24551
|
+
}
|
|
24552
|
+
card.appendChild(slot);
|
|
24553
|
+
|
|
24554
|
+
// A graded finding is the human's word, not the model's: the ledger
|
|
24555
|
+
// carries the corrected label/headline, while the run's report still
|
|
24556
|
+
// holds the original prediction. Upgrading would show the guess the
|
|
24557
|
+
// person explicitly overwrote, so the card keeps the ledger's version.
|
|
24558
|
+
if (source.graded) return finishReasonCard(card, feature, spec, rr);
|
|
24559
|
+
|
|
24560
|
+
fetchRunReport(source.runId).then(function (report) {
|
|
24561
|
+
var reportRow = reportRowFor(report, perspSpecKey(feature, spec));
|
|
24562
|
+
var a = reportRow && reportRow.analysis;
|
|
24563
|
+
if (!a) {
|
|
24564
|
+
if (!source.headline) slot.appendChild(el("div", "d-prose muted", t("perspectives.finding.loadFailed")));
|
|
24565
|
+
return;
|
|
24566
|
+
}
|
|
24567
|
+
// The ledger's one-liner stands in when the report row lost its own.
|
|
24568
|
+
if (!a.headline) a.headline = source.headline || "";
|
|
24569
|
+
clear(slot);
|
|
24570
|
+
var fullHead = diagnosisHead(a);
|
|
24571
|
+
fullHead.appendChild(note);
|
|
24572
|
+
slot.appendChild(fullHead);
|
|
24573
|
+
var fullKv = diagnosisKv(a);
|
|
24574
|
+
if (fullKv) slot.appendChild(fullKv);
|
|
24575
|
+
var evi = analysisEvidenceSection(reportRow);
|
|
24576
|
+
if (evi.count) slot.appendChild(detailsBlock(t("acc.evidence"), evi.count, evi.node));
|
|
24577
|
+
});
|
|
24578
|
+
} else {
|
|
24579
|
+
// No finding to show: the reason is the deploy-log answer, or the
|
|
24580
|
+
// verdict's own wording.
|
|
24581
|
+
card.appendChild(rerunEvidenceValue(rr));
|
|
24582
|
+
}
|
|
24583
|
+
|
|
24584
|
+
return finishReasonCard(card, feature, spec, rr);
|
|
24585
|
+
}
|
|
24586
|
+
|
|
24587
|
+
// The card's shared tail: the dismissal/lapse notes and the person's
|
|
24588
|
+
// controls, appended after whichever body the card ended up with.
|
|
24589
|
+
function finishReasonCard(card, feature, spec, rr) {
|
|
24590
|
+
var dline = rerunDismissalLine(rr);
|
|
24591
|
+
if (dline) card.appendChild(el("div", "d-prose" + (dline.muted ? " muted" : ""), dline.text));
|
|
24592
|
+
var lapse = rerunManualLapseText(rr);
|
|
24593
|
+
if (lapse) card.appendChild(el("div", "d-prose muted", lapse));
|
|
24594
|
+
var auditBox = auditOverrideBox(feature, spec, rr);
|
|
24595
|
+
if (auditBox) card.appendChild(auditBox);
|
|
24596
|
+
var execBox = executionOverrideBox(feature, spec, rr);
|
|
24597
|
+
if (execBox) card.appendChild(execBox);
|
|
24598
|
+
return card;
|
|
24599
|
+
}
|
|
24600
|
+
|
|
24601
|
+
// Detail row: the case's current state first, then why the verdict is what
|
|
24602
|
+
// it is, then what the case does, then the note — a stack of cards in the
|
|
24603
|
+
// order a reader asks the questions. Built with createElement/textContent
|
|
24604
|
+
// throughout — every field here is API-derived, so none of it may go
|
|
24605
|
+
// through innerHTML.
|
|
24144
24606
|
function perspDetailContent(feature, spec) {
|
|
24145
24607
|
var frag = document.createDocumentFragment();
|
|
24608
|
+
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
24609
|
+
var driftEntry = ledgerEntryFor(perspState.drift, feature, spec);
|
|
24610
|
+
|
|
24611
|
+
// The two axis states stay in the table row only — the panel answers why,
|
|
24612
|
+
// not what, so it opens straight on the reason.
|
|
24613
|
+
if (rr) {
|
|
24614
|
+
var reason = el("div", "p-sect");
|
|
24615
|
+
reason.appendChild(el("div", "p-slabel", t("perspectives.d.whyVerdict")));
|
|
24616
|
+
reason.appendChild(perspReasonCard(feature, spec, rr, driftEntry));
|
|
24617
|
+
frag.appendChild(reason);
|
|
24618
|
+
}
|
|
24619
|
+
|
|
24620
|
+
var contents = el("div", "p-sect");
|
|
24621
|
+
contents.appendChild(el("div", "p-slabel", t("perspectives.d.contents")));
|
|
24622
|
+
var ccard = el("div", "analysis-box");
|
|
24146
24623
|
var dl = el("dl", "d-grid");
|
|
24147
24624
|
function row(labelKey, valueNode) {
|
|
24148
24625
|
dl.appendChild(el("dt", null, t(labelKey)));
|
|
@@ -24159,20 +24636,7 @@ const CLIENT_JS = `
|
|
|
24159
24636
|
}
|
|
24160
24637
|
if (spec.startScreen) row("perspectives.d.startScreen", spec.startScreen);
|
|
24161
24638
|
if (spec.testCondition) row("perspectives.d.testCondition", spec.testCondition);
|
|
24162
|
-
// The spec id stays: it is what a user types to re-run this case, and the
|
|
24163
|
-
// table shows the title, never the id.
|
|
24164
|
-
row("perspectives.d.spec", el("code", null, spec.specName));
|
|
24165
|
-
|
|
24166
|
-
var rr = ledgerEntryFor(perspState.rerun, feature, spec);
|
|
24167
|
-
if (rr) {
|
|
24168
|
-
row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
|
|
24169
|
-
if (rerunHasFailure(rr)) row("perspectives.d.lastRed", rerunFailureValue(rr.lastRed));
|
|
24170
|
-
}
|
|
24171
|
-
frag.appendChild(dl);
|
|
24172
|
-
|
|
24173
24639
|
if (spec.steps && spec.steps.length) {
|
|
24174
|
-
var stepsBox = el("div", "steps-box");
|
|
24175
|
-
stepsBox.appendChild(el("div", "slabel", t("perspectives.d.steps")));
|
|
24176
24640
|
var stepsList = el("ol", "d-steps");
|
|
24177
24641
|
spec.steps.forEach(function (step) {
|
|
24178
24642
|
var li = el("li");
|
|
@@ -24186,30 +24650,14 @@ const CLIENT_JS = `
|
|
|
24186
24650
|
}
|
|
24187
24651
|
stepsList.appendChild(li);
|
|
24188
24652
|
});
|
|
24189
|
-
|
|
24190
|
-
frag.appendChild(stepsBox);
|
|
24191
|
-
}
|
|
24192
|
-
|
|
24193
|
-
// A standing attestation always offers revoke — even when it changed
|
|
24194
|
-
// nothing (a held or machine-verified spec), it exists and must stay
|
|
24195
|
-
// findable. A lapsed one is named right here, beside the attest button
|
|
24196
|
-
// the reader is deciding whether to press.
|
|
24197
|
-
var manualBtn = null;
|
|
24198
|
-
if (rr && rr.manual) manualBtn = manualRevokeButton(feature, spec);
|
|
24199
|
-
else if (rr && (rr.verdict === "needsRepair" || rr.verdict === "rerunNeeded")) manualBtn = manualAttestButton(feature, spec);
|
|
24200
|
-
if (manualBtn) {
|
|
24201
|
-
var manualBox = el("div", "manual-attest");
|
|
24202
|
-
if (rr.manual && rr.verdict !== "manuallyVerified") {
|
|
24203
|
-
manualBox.appendChild(el("div", "d-prose", manualAttestationText(rr.manual)));
|
|
24204
|
-
}
|
|
24205
|
-
var lapseLine = rerunManualLapseText(rr);
|
|
24206
|
-
if (lapseLine) manualBox.appendChild(el("div", "d-prose", lapseLine));
|
|
24207
|
-
manualBox.appendChild(manualBtn);
|
|
24208
|
-
frag.appendChild(manualBox);
|
|
24653
|
+
row("perspectives.d.steps", stepsList);
|
|
24209
24654
|
}
|
|
24655
|
+
ccard.appendChild(dl);
|
|
24656
|
+
contents.appendChild(ccard);
|
|
24657
|
+
frag.appendChild(contents);
|
|
24210
24658
|
|
|
24211
|
-
var notebox = el("div", "notebox");
|
|
24212
|
-
notebox.appendChild(el("div", "
|
|
24659
|
+
var notebox = el("div", "notebox p-sect");
|
|
24660
|
+
notebox.appendChild(el("div", "p-slabel", t("perspectives.note.label")));
|
|
24213
24661
|
var ta = el("textarea");
|
|
24214
24662
|
ta.placeholder = t("perspectives.note.placeholder");
|
|
24215
24663
|
ta.value = spec.note || "";
|
|
@@ -24218,8 +24666,8 @@ const CLIENT_JS = `
|
|
|
24218
24666
|
var saveBtn = el("button", "btn primary", t("common.save"));
|
|
24219
24667
|
saveBtn.type = "button";
|
|
24220
24668
|
var statusEl = el("span", "nstatus");
|
|
24221
|
-
nact.appendChild(saveBtn);
|
|
24222
24669
|
nact.appendChild(statusEl);
|
|
24670
|
+
nact.appendChild(saveBtn);
|
|
24223
24671
|
notebox.appendChild(nact);
|
|
24224
24672
|
frag.appendChild(notebox);
|
|
24225
24673
|
|
|
@@ -24275,6 +24723,7 @@ const CLIENT_JS = `
|
|
|
24275
24723
|
|
|
24276
24724
|
var titleTd = el("td", "c-title");
|
|
24277
24725
|
titleTd.appendChild(document.createTextNode(spec.title));
|
|
24726
|
+
titleTd.appendChild(el("span", "c-id", perspSpecKey(feature, spec)));
|
|
24278
24727
|
if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
|
|
24279
24728
|
row.appendChild(titleTd);
|
|
24280
24729
|
|
|
@@ -24407,8 +24856,8 @@ const CLIENT_JS = `
|
|
|
24407
24856
|
loadRerun().catch(function (err) {
|
|
24408
24857
|
setPerspNote("persp-rerun-note", t("perspectives.rerun.loadFailed") + ": " + err.message, "warn");
|
|
24409
24858
|
}),
|
|
24410
|
-
//
|
|
24411
|
-
//
|
|
24859
|
+
// A failed or unsupported fetch just omits the "audited at" line
|
|
24860
|
+
// and the reason card's finding detail, no banner.
|
|
24412
24861
|
loadDrift().catch(function () {}),
|
|
24413
24862
|
]);
|
|
24414
24863
|
})
|
|
@@ -24547,29 +24996,32 @@ const CLIENT_JS = `
|
|
|
24547
24996
|
});
|
|
24548
24997
|
}
|
|
24549
24998
|
|
|
24550
|
-
// Loaded
|
|
24551
|
-
//
|
|
24552
|
-
//
|
|
24553
|
-
//
|
|
24554
|
-
// axis in the /rerun report — so there is nothing here worth a banner on
|
|
24555
|
-
// an older or unreachable hub.
|
|
24999
|
+
// Loaded on project open and again on every reloadRerun: the reason card
|
|
25000
|
+
// joins rr.audit against this ledger's entry (runId/headline), so the two
|
|
25001
|
+
// must not drift apart after a dismissal or attestation. An older or
|
|
25002
|
+
// unreachable hub degrades to the axis alone — not worth a banner.
|
|
24556
25003
|
function loadDrift() {
|
|
24557
25004
|
return fetch(driftPath(), { headers: { Authorization: "Bearer " + state.token } })
|
|
24558
25005
|
.then(function (res) { return res.ok ? res.json() : null; }, function () { return null; })
|
|
24559
25006
|
.then(function (report) {
|
|
24560
|
-
|
|
25007
|
+
// A transient failure keeps the copy already loaded — blanking it
|
|
25008
|
+
// would drop the audit coordinates and the reason card's finding
|
|
25009
|
+
// for the rest of the session.
|
|
25010
|
+
if (report) perspState.drift = report;
|
|
24561
25011
|
renderPerspectives();
|
|
24562
25012
|
});
|
|
24563
25013
|
}
|
|
24564
25014
|
|
|
24565
|
-
//
|
|
24566
|
-
//
|
|
24567
|
-
//
|
|
25015
|
+
// Re-asks the rerun question and refreshes the drift ledger beside it (the
|
|
25016
|
+
// reason card reads both; see perspState.drift). The perspectives document
|
|
25017
|
+
// itself is project-scoped and does not change, and neither does the run
|
|
25018
|
+
// index loadRerun used to (wastefully) re-fetch.
|
|
24568
25019
|
function reloadRerun() {
|
|
24569
25020
|
perspState.rerun = null;
|
|
24570
25021
|
setPerspNote("persp-rerun-note", "");
|
|
24571
25022
|
setPerspDeployHead(null);
|
|
24572
25023
|
renderPerspectives();
|
|
25024
|
+
loadDrift().catch(function () {});
|
|
24573
25025
|
return loadRerun();
|
|
24574
25026
|
}
|
|
24575
25027
|
|
|
@@ -25502,6 +25954,9 @@ function registerRoutes(router, config, queue) {
|
|
|
25502
25954
|
router.get("/api/v1/projects/:project/attestations", createGetAttestationsHandler(storage));
|
|
25503
25955
|
router.put("/api/v1/projects/:project/attestations", createPutAttestationHandler(storage));
|
|
25504
25956
|
router.delete("/api/v1/projects/:project/attestations", createDeleteAttestationHandler(storage));
|
|
25957
|
+
router.get("/api/v1/projects/:project/audit-dismissals", createGetAuditDismissalsHandler(storage));
|
|
25958
|
+
router.put("/api/v1/projects/:project/audit-dismissals", createPutAuditDismissalHandler(storage));
|
|
25959
|
+
router.delete("/api/v1/projects/:project/audit-dismissals", createDeleteAuditDismissalHandler(storage));
|
|
25505
25960
|
router.get("/api/v1/projects/:project/acks/:name", createGetAckHandler(storage));
|
|
25506
25961
|
router.put("/api/v1/projects/:project/acks/:name", createPutAckHandler(storage));
|
|
25507
25962
|
router.post("/api/v1/projects/:project/spend", createRecordSpendHandler(storage));
|
|
@@ -25794,6 +26249,9 @@ function specLocksPath(root, project, profile) {
|
|
|
25794
26249
|
function attestationsPath(root, project, profile) {
|
|
25795
26250
|
return join(root, "attestations", project, profile, "attestations.json");
|
|
25796
26251
|
}
|
|
26252
|
+
function auditDismissalsPath(root, project) {
|
|
26253
|
+
return join(root, "audit-dismissals", `${project}.json`);
|
|
26254
|
+
}
|
|
25797
26255
|
function ackPath(root, project, profile, name) {
|
|
25798
26256
|
return join(root, "acks", project, profile, `${name}.json`);
|
|
25799
26257
|
}
|
|
@@ -25851,6 +26309,22 @@ function createFileAttestationStore(root) {
|
|
|
25851
26309
|
};
|
|
25852
26310
|
}
|
|
25853
26311
|
//#endregion
|
|
26312
|
+
//#region src/hub/core/storage/file/audit-dismissal-store.ts
|
|
26313
|
+
function toDismissals(doc) {
|
|
26314
|
+
const parsed = AuditDismissalsSchema.safeParse(doc);
|
|
26315
|
+
return parsed.success ? parsed.data : { specs: {} };
|
|
26316
|
+
}
|
|
26317
|
+
function createFileAuditDismissalStore(root) {
|
|
26318
|
+
return {
|
|
26319
|
+
async get(project) {
|
|
26320
|
+
return toDismissals(await readJson(auditDismissalsPath(root, project)));
|
|
26321
|
+
},
|
|
26322
|
+
async update(project, mutate) {
|
|
26323
|
+
return updateJson(auditDismissalsPath(root, project), (current) => mutate(toDismissals(current)));
|
|
26324
|
+
}
|
|
26325
|
+
};
|
|
26326
|
+
}
|
|
26327
|
+
//#endregion
|
|
25854
26328
|
//#region src/hub/core/storage/file/artifact-store.ts
|
|
25855
26329
|
/**
|
|
25856
26330
|
* Defense-in-depth: `relPath` is expected to already be validated by the
|
|
@@ -26335,7 +26809,8 @@ function createFileHubStorage(dataDir) {
|
|
|
26335
26809
|
locks: createFileLockStore(dataDir),
|
|
26336
26810
|
acks: createFileAckStore(dataDir),
|
|
26337
26811
|
spend: createFileSpendStore(dataDir),
|
|
26338
|
-
attestations: createFileAttestationStore(dataDir)
|
|
26812
|
+
attestations: createFileAttestationStore(dataDir),
|
|
26813
|
+
auditDismissals: createFileAuditDismissalStore(dataDir)
|
|
26339
26814
|
};
|
|
26340
26815
|
}
|
|
26341
26816
|
//#endregion
|