ccqa 1.33.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 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
- if (endpointEnv["CLAUDE_CODE_OAUTH_TOKEN"]) delete endpointEnv["ANTHROPIC_API_KEY"];
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 hasEndpointEnv = Object.keys(resolveEndpointEnv()).length > 0;
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;
@@ -21079,7 +21104,7 @@ const CSS = `
21079
21104
  .d-grid dt { color: var(--muted); font-size: 12px; padding-top: 1px; }
21080
21105
  .d-grid dd { color: var(--fg-dim); }
21081
21106
  .d-grid dd ul { list-style: none; display: flex; flex-direction: column; gap: 3px; margin: 0; padding: 0; }
21082
- .d-grid dd li::before { content: "\\2022 "; color: var(--muted-2); }
21107
+ .d-grid dd ul li::before { content: "\\2022 "; color: var(--muted-2); }
21083
21108
  .d-grid code { font-size: 12px; background: var(--surface-2); border: 1px solid var(--border); border-radius: 4px; padding: 1px 5px; }
21084
21109
  /* Prose gets a measure so it stops wrapping mid-phrase in a narrow column;
21085
21110
  paths wrap as whole chips, never inside a path. */
@@ -21088,17 +21113,20 @@ const CSS = `
21088
21113
  .d-paths code { white-space: nowrap; }
21089
21114
  .d-prose + .d-paths { margin-top: 6px; }
21090
21115
  .manual-attest { margin-top: 14px; }
21091
- .steps-box { margin-top: 14px; max-width: 900px; }
21092
- .steps-box .slabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
21093
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; }
21094
21117
  .d-steps .step-expected { display: block; margin-top: 2px; font-size: 12.5px; }
21095
- .notebox { margin-top: 14px; max-width: 900px; }
21096
- .notebox .nlabel { font-size: 12px; color: var(--muted); margin-bottom: 4px; }
21097
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; }
21098
- .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; }
21099
21120
  .notebox .nstatus { font-size: 12px; color: var(--muted); }
21100
21121
  .notebox .nstatus.ok { color: var(--pass); }
21101
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); }
21102
21130
  /* The inline form an audit-dismissal or environment-attestation button
21103
21131
  expands into, in place of the two window.prompt() calls this replaces. */
21104
21132
  .override-form { margin-top: 10px; max-width: 480px; display: flex; flex-direction: column; gap: 10px; }
@@ -21226,16 +21254,15 @@ const CLIENT_JS = `
21226
21254
  "perspectives.mode.deterministic": "deterministic", "perspectives.mode.live": "live",
21227
21255
  "perspectives.ov.cases": "cases", "perspectives.ov.features": "features",
21228
21256
  "perspectives.d.preconditions": "Preconditions", "perspectives.d.startScreen": "Start screen",
21229
- "perspectives.d.testCondition": "Condition", "perspectives.d.spec": "spec",
21230
- "perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
21257
+ "perspectives.d.testCondition": "Condition", "perspectives.d.steps": "Steps", "perspectives.d.stepInclude": "Include: {name}",
21231
21258
  "perspectives.d.stepExpected": "Expected:",
21232
21259
  "perspectives.note.label": "Note",
21233
21260
  "perspectives.note.placeholder": "Notes about this case…",
21234
21261
  "perspectives.note.saved": "Saved",
21235
21262
  "perspectives.note.error": "Could not save — retry",
21236
- "perspectives.d.lastRed": "Most recent failure",
21237
- "perspectives.d.changedSince": "Changes since the last run",
21238
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",
21239
21266
  "perspectives.result.openRun": "Open this run in the hub",
21240
21267
  "perspectives.result.ci": "CI",
21241
21268
  "perspectives.rerun.state.needsRepair": "Needs repair",
@@ -21246,26 +21273,26 @@ const CLIENT_JS = `
21246
21273
  "perspectives.rerun.vsDeploy": "judged against deploy",
21247
21274
  "perspectives.rerun.noDeployHead": "no deploy recorded for this profile",
21248
21275
  "perspectives.rerun.changedByDeploy": "deploy {sha} changed files matched to this case",
21249
- "perspectives.rerun.changesSome": "yes (as of deploy {sha})",
21250
- "perspectives.rerun.changesNone": "none (as of deploy {sha})",
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})",
21251
21278
  "perspectives.rerun.touchedCount": "{n} deployed path(s) matched this case",
21252
21279
  "perspectives.rerun.touchedUnknown": "a deploy since the last run matched this case",
21253
21280
  "perspectives.rerun.inProgressHint": "an audit or a run is still going, or the audit has not caught up with the deploy",
21254
- "perspectives.rerun.heldHint": "another job already holds this specacting on it now would race that job",
21255
- "perspectives.rerun.repair.testDrift": "the generated test no longer matches the code — re-record it",
21256
- "perspectives.rerun.repair.specChange": "the spec describes something the code no longer doesa human decides",
21257
- "perspectives.rerun.repair.auditUndecided": "the audit read the code and could not decidea human looks",
21258
- "perspectives.rerun.repair.runFailed": "the last run failed — re-running it changes nothing until the cause is fixed",
21259
- "perspectives.rerun.why.noSelectionInRange": "a deploy in range was recorded without a spec selection",
21260
- "perspectives.rerun.why.selectionUnknown": "the selector could not tell whether this case was affected",
21281
+ "perspectives.rerun.heldHint": "an audit, auto-fix or run job is working on this casedismissing or attesting is unavailable until it finishes",
21282
+ "perspectives.rerun.repair.testDrift": "the audit judged the generated test code older than the implementationauto-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",
21261
21288
  "perspectives.rerun.why.noDeployLog": "no deploy log for this profile",
21262
21289
  "perspectives.rerun.why.unknownDeployedSha": "the last run's deployed commit is unknown",
21263
21290
  "perspectives.rerun.why.ambiguousDeployedSha": "a deploy landed while the last run was executing",
21264
21291
  "perspectives.rerun.why.deployedShaNotInLog": "the last run's commit predates the retained deploy log",
21265
21292
  "perspectives.rerun.why.gapInRange": "deploys are missing from the range",
21266
21293
  "perspectives.rerun.why.unrecognized": "this hub reported a reason this UI does not recognise",
21267
- "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.",
21268
- "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.",
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.",
21269
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.",
21270
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.",
21271
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.",
@@ -21298,8 +21325,8 @@ const CLIENT_JS = `
21298
21325
  "perspectives.override.noteLabel": "What was resolved, and how you checked (required)",
21299
21326
  "perspectives.override.submit": "Record",
21300
21327
  "perspectives.override.cancel": "Never mind",
21301
- "perspectives.override.dismissHint": "Overrides the audit finding and closes the case. The verdict moves to “re-run needed”, and the next run settles it.",
21302
- "perspectives.override.envHint": "The failure stays on record, but the verdict becomes “manually verified without waiting for a re-run. It lapses once a deploy reaches this spec.",
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.",
21303
21330
  "prompt.card.record": "Recording browser actions",
21304
21331
  "prompt.card.live": "Live run (AI-driven)",
21305
21332
  "prompt.card.playwright": "Playwright test generation",
@@ -21416,16 +21443,15 @@ const CLIENT_JS = `
21416
21443
  "perspectives.mode.deterministic": "決定的", "perspectives.mode.live": "ライブ",
21417
21444
  "perspectives.ov.cases": "ケース", "perspectives.ov.features": "機能",
21418
21445
  "perspectives.d.preconditions": "前提条件", "perspectives.d.startScreen": "開始画面",
21419
- "perspectives.d.testCondition": "実行条件", "perspectives.d.spec": "spec",
21420
- "perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
21446
+ "perspectives.d.testCondition": "実行条件", "perspectives.d.steps": "手順", "perspectives.d.stepInclude": "ブロック: {name}",
21421
21447
  "perspectives.d.stepExpected": "期待結果:",
21422
- "perspectives.note.label": "note",
21448
+ "perspectives.note.label": "メモ",
21423
21449
  "perspectives.note.placeholder": "このケースについてのメモ…",
21424
21450
  "perspectives.note.saved": "保存しました",
21425
21451
  "perspectives.note.error": "保存に失敗しました — 再試行してください",
21426
- "perspectives.d.lastRed": "直近の失敗",
21427
- "perspectives.d.changedSince": "前回実行以降の変更",
21428
21452
  "perspectives.d.whyVerdict": "この判定の理由",
21453
+ "perspectives.d.contents": "テストの内容",
21454
+ "perspectives.finding.loadFailed": "詳細を読み込めませんでした。実行のページで確認してください",
21429
21455
  "perspectives.result.openRun": "ハブでこの実行を開く",
21430
21456
  "perspectives.result.ci": "CI",
21431
21457
  "perspectives.rerun.state.needsRepair": "修正待ち",
@@ -21435,27 +21461,27 @@ const CLIENT_JS = `
21435
21461
  "perspectives.rerun.state.verified": "検証済み",
21436
21462
  "perspectives.rerun.vsDeploy": "判定基準: デプロイ",
21437
21463
  "perspectives.rerun.noDeployHead": "このプロファイルにはデプロイの記録がありません",
21438
- "perspectives.rerun.changedByDeploy": "デプロイ {sha} がこのケースに一致するファイルを変更",
21439
- "perspectives.rerun.changesSome": "あり(デプロイ {sha} 時点)",
21440
- "perspectives.rerun.changesNone": "なし(デプロイ {sha} 時点)",
21441
- "perspectives.rerun.touchedCount": "このケースに一致したデプロイ差分 {n} ",
21442
- "perspectives.rerun.touchedUnknown": "前回実行以降のデプロイがこのケースに一致する変更を行っています",
21443
- "perspectives.rerun.inProgressHint": "監査か実行がまだ走っているか、監査がデプロイに追いついていません",
21444
- "perspectives.rerun.heldHint": "このスペックは既に別のジョブが保持しています。今操作するとそのジョブと競合します",
21445
- "perspectives.rerun.repair.testDrift": "生成されたテストが古くなっています。録り直してください",
21446
- "perspectives.rerun.repair.specChange": "spec がコードのやめた動作を書いています。人が判断します",
21447
- "perspectives.rerun.repair.auditUndecided": "監査がコードを読んだうえで判定できませんでした。人が見ます",
21448
- "perspectives.rerun.repair.runFailed": "最後の実行が落ちています。原因を直すまで再実行しても変わりません",
21449
- "perspectives.rerun.why.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあります",
21450
- "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": "直近のデプロイがこのケースに影響するかどうか判断できなかったため、念のため実行対象になっています",
21451
21477
  "perspectives.rerun.why.noDeployLog": "このプロファイルのデプロイ記録がありません",
21452
21478
  "perspectives.rerun.why.unknownDeployedSha": "前回実行時にデプロイされていたcommitが不明です",
21453
21479
  "perspectives.rerun.why.ambiguousDeployedSha": "前回実行の途中でデプロイが発生しました",
21454
- "perspectives.rerun.why.deployedShaNotInLog": "前回実行のcommitが保持中のデプロイログより古いです",
21480
+ "perspectives.rerun.why.deployedShaNotInLog": "前回の実行が古く、どのデプロイに対して実行した結果か特定できません",
21455
21481
  "perspectives.rerun.why.gapInRange": "対象範囲のデプロイ記録が欠けています",
21456
21482
  "perspectives.rerun.why.unrecognized": "このUIが認識できない理由がハブから返されました",
21457
- "perspectives.rerun.fix.noSelectionInRange": "対象範囲に判定を伴わないデプロイがあり、このケースに影響したかどうかを示すものがありません。デプロイジョブで ccqa select-specs を実行し、判定をデプロイと一緒に送ってください。",
21458
- "perspectives.rerun.fix.selectionUnknown": "対象範囲のデプロイは判定されましたが、このケースについては判断がつきませんでした。再実行して基準を取り直してください。",
21483
+ "perspectives.rerun.fix.noSelectionInRange": "影響判定なしで記録されたデプロイがあり、このケースに影響したかどうか分かりません。デプロイの記録時に影響判定が時間内に終わらなかったか、無効化されていたときに起きます。次の実行と監査で結果を取り直します。",
21484
+ "perspectives.rerun.fix.selectionUnknown": "デプロイがこのケースに影響するかどうか判断できなかったため、次の実行で結果を取り直します。",
21459
21485
  "perspectives.rerun.fix.noDeployLog": "このプロファイルのデプロイログに記録がありません。何がデプロイされたかをccqaに伝えるため、この環境のデプロイジョブに ccqa hub deploy record を組み込んでください。",
21460
21486
  "perspectives.rerun.fix.unknownDeployedSha": "前回実行は環境で動いていたcommitを記録していないため、デプロイログ上の位置を決められません。このプロファイルにデプロイログができれば、以降の実行では記録されます。",
21461
21487
  "perspectives.rerun.fix.ambiguousDeployedSha": "前回実行の途中でデプロイが発生したため、どのcommitを検証したのか確定できません。基準を取り直すには再実行してください。",
@@ -21471,25 +21497,25 @@ const CLIENT_JS = `
21471
21497
  "perspectives.manual.envButton": "環境要因が解消した場合はこちら",
21472
21498
  "perspectives.manual.confirmRevoke": "このケースの手動確認を取り消しますか?",
21473
21499
  "perspectives.manual.error": "保存に失敗しました — 再試行してください",
21474
- "perspectives.manual.verifiedBy": "{by}さんが手動確認({at})",
21475
- "perspectives.manual.lapsed.deployReached": "手動確認はデプロイの到達により失効",
21476
- "perspectives.manual.lapsed.deployReachedNamed": "手動確認はデプロイ {sha}({at})の到達により失効",
21477
- "perspectives.manual.lapsed.cannotPlace": "手動確認は基準デプロイをログで特定できず失効",
21478
- "perspectives.manual.lapsed.specEdited": "手動確認はspecの編集により失効",
21479
- "perspectives.manual.lapsed.newerRed": "手動確認は直後の実行失敗により失効",
21480
- "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 が認識できない理由により、手動確認は失効しました",
21481
21507
  "perspectives.dismiss.offerButton": "テスト仕様に問題がなかった場合はこちら",
21482
21508
  "perspectives.dismiss.revokeButton": "棄却を取り消す",
21483
21509
  "perspectives.dismiss.confirmRevoke": "このケースの棄却を取り消しますか?",
21484
- "perspectives.dismiss.activeNote": "監査指摘「{headline}」は{by}が誤検知として棄却({at})—「{note}」。次の実行が裁定します。",
21485
- "perspectives.dismiss.priorNote": "前回この指摘は{by}が棄却しています({at})—「{note}",
21510
+ "perspectives.dismiss.activeNote": "{by} さんが監査指摘「{headline}」を誤検知として棄却しました({at}・理由: {note})。次の実行結果が正否を決めます。",
21511
+ "perspectives.dismiss.priorNote": "この指摘は、以前 {by} さんが棄却しています({at}・理由: {note}",
21486
21512
  "perspectives.override.byLabel": "確認した人",
21487
21513
  "perspectives.override.reasonLabel": "理由(必須)",
21488
21514
  "perspectives.override.noteLabel": "解消と確認の内容(必須)",
21489
21515
  "perspectives.override.submit": "記録する",
21490
21516
  "perspectives.override.cancel": "やめる",
21491
- "perspectives.override.dismissHint": "監査の指摘を上書きして台帳を閉じます。判定は「要再実行」に移り、次の実行が正否を裁定します。",
21492
- "perspectives.override.envHint": "失敗の記録は残したまま、判定は再実行を待たず「手動確認済み」になります。次のデプロイがこの spec に届くと失効します。",
21517
+ "perspectives.override.dismissHint": "監査の指摘を取り下げて、このケースを実行対象に戻します。判定は「要再実行」になり、次の実行結果が正否を決めます。",
21518
+ "perspectives.override.envHint": "失敗の記録は残したまま、判定だけが「手動確認済み」になります。次のデプロイがこのケースに届くと失効し、通常の実行に戻ります。",
21493
21519
  "prompt.card.record": "ブラウザ操作の記録",
21494
21520
  "prompt.card.live": "ライブ実行(AI操作)",
21495
21521
  "prompt.card.playwright": "Playwrightテスト生成",
@@ -21786,7 +21812,6 @@ const CLIENT_JS = `
21786
21812
  return span;
21787
21813
  }
21788
21814
 
21789
-
21790
21815
  // Shared by the runs-list row and the run-detail header — the one place
21791
21816
  // both decide whether a run's own status badge speaks drift's vocabulary.
21792
21817
  function runStatusBadge(run) {
@@ -22396,15 +22421,13 @@ const CLIENT_JS = `
22396
22421
 
22397
22422
  // ── run detail: spec cards ──────────────────────────────────────────
22398
22423
 
22399
- // The diagnosis card: one surface for everything about a failure's cause.
22400
- // Verdict (label + confidence), then the cause→fix pair as labelled rows —
22401
- // headline and recommendation are one causal unit, so they read as one.
22402
- // subDiagnosis is deliberately NOT shown: it is a machine vocabulary for
22403
- // accuracy stratification and learning, not for humans. The caller appends
22404
- // the evidence/reasoning accordions and the grading zone into this box.
22405
- function analysisSection(runId, r) {
22406
- var wrap = el("div", "analysis-box");
22407
- 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) {
22408
22431
  var head = el("div", "analysis-head");
22409
22432
  head.appendChild(labelChip(a.label));
22410
22433
  // Which repair a SPEC_CHANGE needs — delete the spec, or rewrite it. The
@@ -22414,7 +22437,10 @@ const CLIENT_JS = `
22414
22437
  head.appendChild(el("span", "chip spec-change-chip", t("diag.specChangeKind." + a.specChangeKind)));
22415
22438
  }
22416
22439
  head.appendChild(el("span", "conf", Math.round(a.confidence * 100) + "%"));
22417
- wrap.appendChild(head);
22440
+ return head;
22441
+ }
22442
+
22443
+ function diagnosisKv(a) {
22418
22444
  var kv = el("div", "analysis-kv");
22419
22445
  // Set only when the verdict blames the test case (TEST_DRIFT/SPEC_CHANGE),
22420
22446
  // on both kinds of row — it names the half that has to be repaired.
@@ -22430,7 +22456,14 @@ const CLIENT_JS = `
22430
22456
  kv.appendChild(el("div", "k", t("diag.fix")));
22431
22457
  kv.appendChild(el("div", "v", a.recommendation));
22432
22458
  }
22433
- if (kv.childNodes.length > 0) wrap.appendChild(kv);
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);
22434
22467
  return wrap;
22435
22468
  }
22436
22469
 
@@ -23543,9 +23576,10 @@ const CLIENT_JS = `
23543
23576
  // this hub answers at all. Chip visibility follows it rather than the report,
23544
23577
  // so switching profile doesn't drop the filter while the next one loads.
23545
23578
  // "drift" is the DriftLedgerResponse, or null when unanswered (older hub, or
23546
- // a failed fetch) — not profile-scoped, so it does not reset when the
23547
- // profile switcher changes (unlike "rerun" above). Only feeds the audit
23548
- // column's "audited at" line now; its own finding is superseded by rr.audit.
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.
23549
23583
  var perspState = {
23550
23584
  doc: null, q: "", f: "all",
23551
23585
  rerun: null, rerunSupported: null, runUrls: {}, rerunProfiles: [],
@@ -23613,10 +23647,10 @@ const CLIENT_JS = `
23613
23647
 
23614
23648
  // ── perspectives: drift ledger ────────────────────────────────────────
23615
23649
  // Not profile-scoped (see perspState.drift above), so unlike rerunPath this
23616
- // takes no ?profile=. Its own finding is superseded by the audit axis in
23617
- // the /rerun report (ADR-0014); what survives into the view is only its
23618
- // coordinate when a spec was last audited — folded into the audit
23619
- // column's evidence line (perspAuditCell).
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.
23620
23654
 
23621
23655
  function driftPath() {
23622
23656
  return "/api/v1/projects/" + encodeURIComponent(state.project) + "/drift";
@@ -23669,7 +23703,10 @@ const CLIENT_JS = `
23669
23703
  // when it has no entry).
23670
23704
  function rerunReasonText(prefix, reason) {
23671
23705
  var text = t(prefix + reason);
23672
- return text === prefix + reason ? t(prefix + "unrecognized") : text;
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"));
23673
23710
  }
23674
23711
 
23675
23712
  // Who attested and when, plus their note if they left one — the whole
@@ -23760,7 +23797,6 @@ const CLIENT_JS = `
23760
23797
  return lapse ? why + " · " + lapse : why;
23761
23798
  }
23762
23799
 
23763
-
23764
23800
  // --- pure: rerun composition ---------------------------------------------
23765
23801
  // Self-contained on purpose: no DOM, no closures. rerun-view.test.ts lifts
23766
23802
  // this region out of the rendered page and runs it, because the summary bar
@@ -24202,8 +24238,8 @@ const CLIENT_JS = `
24202
24238
 
24203
24239
  // --- pure: rerun detail labels -------------------------------------------
24204
24240
  // Self-contained on purpose (no DOM, no closures) so rerun-view.test.ts can
24205
- // lift this region out of the rendered page and run it: which label the
24206
- // panel's evidence row wears, and whether it has a failure row at all.
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.
24207
24243
 
24208
24244
  // The deploy log answered for this case: the row can show what it holds.
24209
24245
  // A case the log could not place has no evidence to show even though its
@@ -24213,20 +24249,6 @@ const CLIENT_JS = `
24213
24249
  return rr.verdict === "rerunNeeded" && !rr.executionAssumedReached;
24214
24250
  }
24215
24251
 
24216
- // Evidence is labelled by the timeframe it covers; everything else names why
24217
- // the verdict landed — a different kind of content, and forcing one label
24218
- // over both would make one of the two read as a lie.
24219
- function rerunEvidenceLabelKey(rr) {
24220
- return rerunHasEvidence(rr) ? "perspectives.d.changedSince" : "perspectives.d.whyVerdict";
24221
- }
24222
-
24223
- // The failure row points at a run. With no failure there is nothing to point
24224
- // at, so the row is omitted rather than filled with "never failed" — the row
24225
- // above already carries the last result.
24226
- function rerunHasFailure(rr) {
24227
- return !!(rr && rr.lastRed);
24228
- }
24229
-
24230
24252
  // Which deploy the evidence line names, and how. A "needed" verdict carries
24231
24253
  // the deploy that caused it (touchedByDeploy) when the hub could confirm one,
24232
24254
  // and that is the deploy a reader wants — so it is named, with when it
@@ -24293,13 +24315,10 @@ const CLIENT_JS = `
24293
24315
  return null;
24294
24316
  }
24295
24317
 
24296
- // The evidence behind the verdict, as the value of whichever row
24297
- // rerunEvidenceLabelKey chose. For needed/notNeeded that is what the deploy
24298
- // log holds since this case last ran, named by rerunChangeLine.
24299
- // The label already states the timeframe, so the value never repeats it.
24300
- // A dismissal (active or superseded by a later finding) is appended below
24301
- // whichever of those this case has, rather than replacing it — see
24302
- // rerunDismissalLine.
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.
24303
24322
  function rerunEvidenceValue(rr) {
24304
24323
  var wrap = el("div");
24305
24324
  if (!rerunHasEvidence(rr)) {
@@ -24319,29 +24338,12 @@ const CLIENT_JS = `
24319
24338
  wrap.appendChild(pathCodes(rr.touchedBy));
24320
24339
  }
24321
24340
  }
24322
- var dismissLine = rerunDismissalLine(rr);
24323
- if (dismissLine) wrap.appendChild(el("div", "d-prose" + (dismissLine.muted ? " muted" : ""), dismissLine.text));
24324
- return wrap;
24325
- }
24326
-
24327
- // The failure: which run it was, then what the analysis concluded. The
24328
- // headline is model output, already localized server-side, so it is shown as
24329
- // written. A run made without failure analysis carries neither field, and the
24330
- // row is then the coordinate alone — what it has always been.
24331
- function rerunFailureValue(entry) {
24332
- var wrap = el("div");
24333
- wrap.appendChild(ledgerLine(entry));
24334
- if (entry.label) {
24335
- var line = el("div", "d-prose", labelText(entry.label));
24336
- if (entry.headline) line.appendChild(document.createTextNode(" · " + entry.headline));
24337
- wrap.appendChild(line);
24338
- }
24339
24341
  return wrap;
24340
24342
  }
24341
24343
 
24342
24344
  // Lets a person's own check stand in for the machine's verdict. reloadRerun()
24343
- // is the same profile-scoped refresh a profile switch uses — it re-renders
24344
- // the whole table, so an open detail panel closes along with it.
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.
24345
24347
  function submitAttestation(method, body) {
24346
24348
  apiFetch(attestationsPath(), {
24347
24349
  method: method,
@@ -24458,7 +24460,10 @@ const CLIENT_JS = `
24458
24460
  // dismissal that is currently the reason the axis reads clean. At most one
24459
24461
  // of the two ever shows — a finding the axis
24460
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.
24461
24465
  function auditOverrideBox(feature, spec, rr) {
24466
+ if (rr.heldBy) return null;
24462
24467
  if (auditDismissalActive(rr)) {
24463
24468
  var box = el("div", "manual-attest");
24464
24469
  box.appendChild(auditDismissalRevokeButton(feature, spec));
@@ -24473,6 +24478,7 @@ const CLIENT_JS = `
24473
24478
  // no open finding of its own, which is auditOverrideBox's problem to answer,
24474
24479
  // not this one's.
24475
24480
  function executionOverrideBox(feature, spec, rr) {
24481
+ if (rr.heldBy) return null;
24476
24482
  if (rr.manual) {
24477
24483
  var box = el("div", "manual-attest");
24478
24484
  if (rr.verdict !== "manuallyVerified") box.appendChild(el("div", "d-prose", manualAttestationText(rr.manual)));
@@ -24487,16 +24493,133 @@ const CLIENT_JS = `
24487
24493
  return null;
24488
24494
  }
24489
24495
 
24490
- // Detail row: a definition list of the case's fields plus the note editor.
24491
- // Built with createElement/textContent throughout every field here is
24492
- // API-derived, so none of it may go through innerHTML.
24493
- //
24494
- // The panel shows only what the table row cannot. The row already carries the
24495
- // title, mode, recorded state, last result and the re-run verdict, so none of
24496
- // those is repeated: what is left is the case's definition, the evidence the
24497
- // verdict rests on, and the note.
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.
24498
24606
  function perspDetailContent(feature, spec) {
24499
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");
24500
24623
  var dl = el("dl", "d-grid");
24501
24624
  function row(labelKey, valueNode) {
24502
24625
  dl.appendChild(el("dt", null, t(labelKey)));
@@ -24513,20 +24636,7 @@ const CLIENT_JS = `
24513
24636
  }
24514
24637
  if (spec.startScreen) row("perspectives.d.startScreen", spec.startScreen);
24515
24638
  if (spec.testCondition) row("perspectives.d.testCondition", spec.testCondition);
24516
- // The spec id stays: it is what a user types to re-run this case, and the
24517
- // table shows the title, never the id.
24518
- row("perspectives.d.spec", el("code", null, spec.specName));
24519
-
24520
- var rr = ledgerEntryFor(perspState.rerun, feature, spec);
24521
- if (rr) {
24522
- row(rerunEvidenceLabelKey(rr), rerunEvidenceValue(rr));
24523
- if (rerunHasFailure(rr)) row("perspectives.d.lastRed", rerunFailureValue(rr.lastRed));
24524
- }
24525
- frag.appendChild(dl);
24526
-
24527
24639
  if (spec.steps && spec.steps.length) {
24528
- var stepsBox = el("div", "steps-box");
24529
- stepsBox.appendChild(el("div", "slabel", t("perspectives.d.steps")));
24530
24640
  var stepsList = el("ol", "d-steps");
24531
24641
  spec.steps.forEach(function (step) {
24532
24642
  var li = el("li");
@@ -24540,22 +24650,14 @@ const CLIENT_JS = `
24540
24650
  }
24541
24651
  stepsList.appendChild(li);
24542
24652
  });
24543
- stepsBox.appendChild(stepsList);
24544
- frag.appendChild(stepsBox);
24653
+ row("perspectives.d.steps", stepsList);
24545
24654
  }
24655
+ ccard.appendChild(dl);
24656
+ contents.appendChild(ccard);
24657
+ frag.appendChild(contents);
24546
24658
 
24547
- // A person's override, always at most one control per axis: which finding
24548
- // is open decides whether that slot offers a new override or revokes a
24549
- // standing one (auditOverrideBox / executionOverrideBox).
24550
- if (rr) {
24551
- var auditBox = auditOverrideBox(feature, spec, rr);
24552
- if (auditBox) frag.appendChild(auditBox);
24553
- var execBox = executionOverrideBox(feature, spec, rr);
24554
- if (execBox) frag.appendChild(execBox);
24555
- }
24556
-
24557
- var notebox = el("div", "notebox");
24558
- notebox.appendChild(el("div", "nlabel", t("perspectives.note.label")));
24659
+ var notebox = el("div", "notebox p-sect");
24660
+ notebox.appendChild(el("div", "p-slabel", t("perspectives.note.label")));
24559
24661
  var ta = el("textarea");
24560
24662
  ta.placeholder = t("perspectives.note.placeholder");
24561
24663
  ta.value = spec.note || "";
@@ -24564,8 +24666,8 @@ const CLIENT_JS = `
24564
24666
  var saveBtn = el("button", "btn primary", t("common.save"));
24565
24667
  saveBtn.type = "button";
24566
24668
  var statusEl = el("span", "nstatus");
24567
- nact.appendChild(saveBtn);
24568
24669
  nact.appendChild(statusEl);
24670
+ nact.appendChild(saveBtn);
24569
24671
  notebox.appendChild(nact);
24570
24672
  frag.appendChild(notebox);
24571
24673
 
@@ -24621,6 +24723,7 @@ const CLIENT_JS = `
24621
24723
 
24622
24724
  var titleTd = el("td", "c-title");
24623
24725
  titleTd.appendChild(document.createTextNode(spec.title));
24726
+ titleTd.appendChild(el("span", "c-id", perspSpecKey(feature, spec)));
24624
24727
  if (spec.summary) titleTd.appendChild(el("span", "csum", spec.summary));
24625
24728
  row.appendChild(titleTd);
24626
24729
 
@@ -24753,8 +24856,8 @@ const CLIENT_JS = `
24753
24856
  loadRerun().catch(function (err) {
24754
24857
  setPerspNote("persp-rerun-note", t("perspectives.rerun.loadFailed") + ": " + err.message, "warn");
24755
24858
  }),
24756
- // Evidence-only (the "audited at" line in the audit column) — a
24757
- // failed or unsupported fetch just omits that line, no banner.
24859
+ // A failed or unsupported fetch just omits the "audited at" line
24860
+ // and the reason card's finding detail, no banner.
24758
24861
  loadDrift().catch(function () {}),
24759
24862
  ]);
24760
24863
  })
@@ -24893,29 +24996,32 @@ const CLIENT_JS = `
24893
24996
  });
24894
24997
  }
24895
24998
 
24896
- // Loaded once per project open never re-run on a profile switch, since
24897
- // drift carries no profile (unlike loadRerun/reloadRerun below). Only the
24898
- // ledger's own coordinate (when a spec was last audited) survives into the
24899
- // view now its finding is superseded by the fresher, deploy-aware audit
24900
- // axis in the /rerun report — so there is nothing here worth a banner on
24901
- // 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.
24902
25003
  function loadDrift() {
24903
25004
  return fetch(driftPath(), { headers: { Authorization: "Bearer " + state.token } })
24904
25005
  .then(function (res) { return res.ok ? res.json() : null; }, function () { return null; })
24905
25006
  .then(function (report) {
24906
- perspState.drift = report || null;
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;
24907
25011
  renderPerspectives();
24908
25012
  });
24909
25013
  }
24910
25014
 
24911
- // Switching profile re-asks only the profile-scoped question: the
24912
- // perspectives document itself is project-scoped and does not change, and
24913
- // neither does the run index loadRerun used to (wastefully) re-fetch.
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.
24914
25019
  function reloadRerun() {
24915
25020
  perspState.rerun = null;
24916
25021
  setPerspNote("persp-rerun-note", "");
24917
25022
  setPerspDeployHead(null);
24918
25023
  renderPerspectives();
25024
+ loadDrift().catch(function () {});
24919
25025
  return loadRerun();
24920
25026
  }
24921
25027
 
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccqa",
3
- "version": "1.33.0",
3
+ "version": "1.34.0",
4
4
  "type": "module",
5
5
  "description": "Browser test recorder powered by Claude Code and agent-browser",
6
6
  "repository": {