@tea-agent/loop-agent 0.21.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +42 -108
- package/CHANGELOG.md +114 -0
- package/README.md +7 -4
- package/bin/agent-worker.js +0 -0
- package/dist/adapters/loop-agent.js +52 -0
- package/dist/application/context-usage/skill-resolution-stats.js +263 -0
- package/dist/application/dag/generate-task-dag.js +17 -3
- package/dist/cli/command-definitions.js +8 -7
- package/dist/cli/program.js +17 -15
- package/dist/commands/doctor.js +269 -18
- package/dist/commands/init.js +198 -86
- package/dist/commands/stats.js +40 -11
- package/dist/executors/dag-pi-executor.js +2 -0
- package/dist/executors/shell-executor.js +162 -19
- package/dist/shared/openspec-spec.js +49 -0
- package/dist/shared/operator/capabilities.js +11 -1
- package/dist/worker/console/app-data.js +4 -0
- package/dist/worker/console/chat/instruction-skills.js +217 -0
- package/dist/worker/console/chat/model-resolver.js +106 -0
- package/dist/worker/console/chat/pi-runtime.js +605 -0
- package/dist/worker/console/chat/resource-loader.js +66 -0
- package/dist/worker/console/chat/routes.js +357 -0
- package/dist/worker/console/chat/session-store.js +238 -0
- package/dist/worker/console/chat/tool-adapter.js +238 -0
- package/dist/worker/console/chat/tools.js +171 -0
- package/dist/worker/console/server.js +55 -0
- package/dist/worker/console/static/assets/index-DVl7Jxt5.js +25 -0
- package/dist/worker/console/static/assets/index-lVcIr9Ju.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/feature/profile-schema.js +1 -1
- package/dist/worker/observability/read-model.js +21 -1
- package/dist/worker/observe/spec-evidence.js +12 -15
- package/dist/worker/observe/static/dag-helpers.js +22 -0
- package/dist/worker/observe/static/views/dag.js +5 -0
- package/dist/workflows/dag/backend-test-markdown-workflow.js +37 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +141 -32
- package/dist/workflows/dag/frontend-lint-baseline.js +471 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +79 -16
- package/dist/workflows/dag/frontend-project-capability.js +11 -8
- package/dist/workflows/dag/frontend-repair.js +6 -4
- package/dist/workflows/dag/frontend-review-context.js +67 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +105 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +71 -66
- package/dist/workflows/dag/frontend-verification-trace.js +31 -1
- package/dist/workflows/dag/frontend-worktree-diff.js +81 -6
- package/dist/workflows/dag/init-hybrid.js +412 -84
- package/dist/workflows/dag/node-execution.js +38 -1
- package/dist/workflows/dag/output-protocol.js +89 -0
- package/dist/workflows/dag/prompt.js +35 -1
- package/dist/workflows/dag/recovery-recommendation.js +45 -0
- package/dist/workflows/dag/report.js +28 -1
- package/dist/workflows/dag/rerun-task.js +1 -1
- package/dist/workflows/dag/scheduler.js +9 -0
- package/dist/workflows/dag/types.js +74 -1
- package/dist/workflows/dag/validate.js +55 -0
- package/docs/README.md +73 -156
- package/docs/architecture/README.md +3 -2
- package/docs/architecture/dag-execution.md +2 -2
- package/docs/architecture/evolution.md +14 -12
- package/docs/architecture/system-overview.md +1 -1
- package/docs/architecture/worker-and-feature.md +3 -3
- package/docs/governance/README.md +15 -0
- package/docs/{harness-methodology-debugging.md → governance/harness-methodology-debugging.md} +27 -3
- package/docs/init-surface.manifest.json +22 -4
- package/docs/operations/README.md +12 -0
- package/docs/{local-development-environment.md → operations/local-development-environment.md} +1 -1
- package/docs/skills/vetted-skill-registry.md +23 -3
- package/docs/templates/README.md +55 -0
- package/docs/templates/agent-dag.schema.json +15 -5
- package/docs/templates/backend-test-dag.json +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-slim-v1.md +87 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.candidate.json +9 -0
- package/docs/templates/evaluation/agents-map-verbose-v0.md +153 -0
- package/docs/templates/frontend-implementation-contract.schema.json +4 -3
- package/docs/templates/frontend-test-case-checklist.md +6 -2
- package/docs/templates/frontend-test-dag.json +2 -2
- package/docs/templates/hybrid-dag.json +1 -1
- package/docs/templates/progress-log.md +9 -2
- package/harness.json +5 -5
- package/package.json +5 -5
- package/scripts/kb-bootstrap-init-skeleton.sh +2 -2
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/frontend-design-review/SKILL.md +12 -10
- package/skills/frontend-design-review/references/review-checklist.md +4 -4
- package/skills/frontend-implementation/SKILL.md +2 -2
- package/skills/frontend-implementation/references/code-standards.md +4 -3
- package/skills/frontend-implementation/references/design-spec.md +19 -14
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/skills/frontend-review/SKILL.md +15 -28
- package/skills/frontend-review/references/review-findings.md +16 -18
- package/skills/frontend-verification/SKILL.md +16 -13
- package/skills/frontend-verification/references/verification-checklist.md +18 -30
- package/skills/grill-with-docs/SKILL.md +44 -52
- package/skills/grill-with-docs/adr-format.md +37 -26
- package/skills/grill-with-docs/context-format.md +18 -26
- package/skills/loop-agent/SKILL.md +28 -112
- package/skills/loop-agent/references/command-reference.md +9 -3
- package/skills/loop-agent/references/harness-policy.md +3 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/orchestrator-and-interventions.md +1 -1
- package/skills/loop-agent/references/task-workflow.md +2 -0
- package/skills/systematic-debugging/SKILL.md +20 -4
- package/skills/test-driven-development/SKILL.md +10 -3
- package/dist/worker/console/static/assets/index-CUDke82y.js +0 -18
- package/dist/worker/console/static/assets/index-wSEksVSO.css +0 -1
- /package/docs/{harness-methodology-tdd.md → governance/harness-methodology-tdd.md} +0 -0
- /package/docs/{harness-methodology-verification.md → governance/harness-methodology-verification.md} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--canvas: #f7f7f4;--canvas-soft: #fafaf7;--surface: #ffffff;--surface-muted: #f0efe9;--ink: #26251e;--body: #5a5852;--muted: #807d72;--muted-soft: #a09c92;--hairline: #e6e5e0;--hairline-strong: #cfcdc4;--orange: #f54e00;--orange-active: #d04200;--orange-soft: color-mix(in srgb, var(--orange) 12%, white);--blue: #3f6ea4;--green: #1f8a65;--amber: #a66a00;--red: #c72d53;--peach: #f1d8cb;--mint: #d8ead5;--space-1: 4px;--space-2: 8px;--space-3: 12px;--space-4: 16px;--space-5: 20px;--space-6: 24px;--space-8: 32px;--radius-sm: 6px;--radius-md: 8px;--radius-lg: 12px;--radius-pill: 999px;--transition-fast: .16s ease;--transition-base: .22s ease;--shadow-panel: 0 1px 2px rgb(38 37 30 / 4%), 0 8px 24px rgb(38 37 30 / 3%);--shadow-soft: 0 10px 30px rgb(38 37 30 / 6%);color-scheme:light;font-family:Segoe UI,PingFang SC,Hiragino Sans GB,Noto Sans SC,system-ui,-apple-system,sans-serif;line-height:1.5}*{box-sizing:border-box}html{background:radial-gradient(1200px 480px at 8% -10%,rgb(241 216 203 / 45%),transparent 55%),radial-gradient(900px 420px at 100% 0%,rgb(216 234 213 / 35%),transparent 50%),var(--canvas);min-height:100%}body{margin:0;min-height:100vh;min-width:320px;background:transparent;color:var(--ink);font-size:14px}button,input,select,textarea{font:inherit}a{color:inherit}code,.cmd,.kv dd code{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace}.embedded-operator-nav{position:sticky;top:0;z-index:40;display:flex;gap:6px;align-items:center;margin:0;padding:8px max(24px,calc((100vw - 1120px)/2));border-bottom:1px solid var(--hairline);background:color-mix(in srgb,var(--canvas) 92%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);font-size:13px;font-weight:700}.embedded-operator-nav a,.embedded-operator-nav span{padding:6px 12px;border-radius:var(--radius-pill);text-decoration:none;color:var(--ink)}.embedded-operator-nav a:hover,.embedded-operator-nav a:focus-visible{background:var(--surface);color:var(--orange-active);outline:none}.embedded-operator-nav span[aria-current=page]{background:var(--ink);color:#fff}.console-shell{width:min(100% - 48px,1120px);margin:0 auto;padding:var(--space-5) 0 var(--space-12, 48px)}.console-header{display:grid;gap:var(--space-4);grid-template-columns:1fr auto;align-items:center;margin-bottom:var(--space-4);padding-bottom:var(--space-4);border-bottom:1px solid var(--hairline)}.brand-block{display:grid;gap:2px;align-content:start;min-width:0}h1{margin:0;color:var(--ink);font-size:clamp(20px,2.2vw,24px);font-weight:650;line-height:1.2;letter-spacing:-.02em}.subtitle{max-width:58ch;margin:0;color:var(--body);font-size:13px;line-height:1.4}.top-nav{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2);margin:0 0 var(--space-5);padding:6px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:color-mix(in srgb,var(--surface) 88%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:var(--shadow-panel)}.nav-link{appearance:none;display:inline-flex;align-items:center;min-height:34px;padding:0 14px;border:1px solid transparent;border-radius:var(--radius-pill);background:transparent;color:var(--body);font-size:13px;font-weight:600;cursor:pointer;transition:background-color var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.nav-link:hover:not(:disabled),.nav-link:focus-visible:not(:disabled){background:var(--surface);color:var(--ink);outline:none}.nav-link-active{background:var(--ink);color:#fff}.nav-link-active:hover:not(:disabled),.nav-link-active:focus-visible:not(:disabled){background:var(--ink);color:#fff}.nav-link-external{margin-left:auto;border-color:var(--hairline);background:var(--surface);color:var(--orange-active)}.nav-link-external:hover:not(:disabled){border-color:var(--orange);background:var(--orange-soft);color:var(--orange-active)}.readiness{position:relative;display:grid;gap:2px;min-width:140px;max-width:280px;border:1px solid var(--hairline);background:var(--surface);border-radius:var(--radius-md);padding:8px 32px 8px 12px;box-shadow:var(--shadow-panel)}.readiness:before{content:"";position:absolute;top:50%;right:12px;width:8px;height:8px;border-radius:50%;transform:translateY(-50%);background:var(--muted-soft)}.readiness[data-state=setup-required]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=setup-required]:before{background:var(--amber)}.readiness[data-state=ready]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=ready]:before{background:color-mix(in srgb,var(--green) 72%,white);box-shadow:0 0 0 3px color-mix(in srgb,var(--green) 12%,transparent)}.readiness[data-state=degraded]{border-color:var(--hairline);background:var(--surface)}.readiness[data-state=degraded]:before{background:var(--amber);box-shadow:0 0 0 3px color-mix(in srgb,var(--amber) 12%,transparent)}.readiness-label{display:block;margin:0;color:var(--muted);font-size:11px;font-weight:600;line-height:1.2}.readiness strong{color:var(--ink);font-size:14px;font-weight:700;line-height:1.2}.readiness[data-state=ready] strong,.readiness[data-state=setup-required] strong,.readiness[data-state=degraded] strong{color:var(--ink)}.readiness .muted{margin:2px 0 0;font-size:12px;line-height:1.35}.flash{display:flex;gap:var(--space-3);align-items:flex-start;margin:0 0 var(--space-4);padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);animation:flash-in .22s ease}.flash-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 6%,var(--surface));color:var(--red)}.flash-info{border-color:color-mix(in srgb,var(--blue) 30%,var(--hairline));background:color-mix(in srgb,var(--blue) 6%,var(--surface));color:var(--body)}.flash-label{flex:0 0 auto;min-width:3.5rem;font-size:12px;font-weight:700;letter-spacing:.02em}.flash-body{margin:0;overflow-wrap:anywhere}.muted{color:var(--muted)}.console-main{display:grid;gap:var(--space-4)}.inspect-split-panel{position:fixed;z-index:50;top:16px;right:16px;bottom:16px;width:min(420px,calc(100vw - 32px));display:grid;align-content:start;gap:var(--space-4);padding:var(--space-5);overflow:auto;border:1px solid var(--hairline);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-soft)}.inspect-split-nodes{display:grid;gap:6px;margin:0;padding:0;list-style:none;max-height:220px;overflow:auto}.inspect-split-nodes li{display:grid;grid-template-columns:1fr auto;gap:var(--space-2);align-items:center;padding:6px 8px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:var(--canvas-soft);font-size:12px}.inspect-split-nodes li.is-active{border-color:var(--orange);background:var(--orange-soft)}.inspect-split-nodes code{color:var(--muted);font-size:11px}.inspect-split-header{display:flex;align-items:start;justify-content:space-between;gap:var(--space-3)}.inspect-split-header h2,.inspect-split-panel h3{margin:0}.inspect-split-kicker,.inspect-split-id{margin:0;color:var(--muted);font-size:12px}.inspect-split-facts{display:grid;gap:var(--space-2);margin:0}.inspect-split-facts div{display:grid;grid-template-columns:7rem 1fr;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--hairline)}.inspect-split-facts dt{color:var(--muted)}.inspect-split-facts dd{margin:0;overflow-wrap:anywhere}.inspect-split-events{display:grid;gap:8px;margin:0;padding-left:18px;color:var(--body);font-size:12px}.panel{min-width:0;border:1px solid var(--hairline);background:color-mix(in srgb,var(--surface) 96%,transparent);border-radius:var(--radius-lg);padding:var(--space-6);box-shadow:var(--shadow-panel)}.panel h2{margin:0 0 var(--space-2);color:var(--ink);font-size:18px;font-weight:700;letter-spacing:-.01em}.panel-lead{margin:0 0 var(--space-5);color:var(--muted);font-size:13px}.panel h3{margin:0 0 var(--space-3);color:var(--ink);font-size:15px;font-weight:700}.empty-state{border:1px dashed var(--hairline-strong);border-radius:var(--radius-md);padding:var(--space-6);background:var(--canvas-soft);color:var(--body)}.empty-title{margin:0 0 var(--space-2);color:var(--ink);font-size:16px;font-weight:700}.checklist{margin:0 0 var(--space-4);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);list-style:none;color:var(--body)}.checklist li{position:relative;padding:6px 0 6px 22px}.checklist li:before{content:"";position:absolute;left:0;top:12px;width:8px;height:8px;border-radius:50%;background:var(--orange);opacity:.75}.kv{display:grid;gap:var(--space-1);margin:0}.kv div{display:grid;grid-template-columns:9rem 1fr;gap:var(--space-3);padding:10px 0;border-bottom:1px solid var(--hairline)}.kv div:last-child{border-bottom:none}.kv dt{color:var(--muted);font-size:12px;font-weight:600}.kv dd{margin:0;color:var(--ink);overflow-wrap:anywhere}code{font-size:.85rem;color:var(--ink)}.cmd{display:block;width:100%;margin:0;padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:#f3efe8;color:var(--ink);font-size:12px;line-height:1.55;overflow-x:auto;white-space:pre-wrap;word-break:break-word;max-height:280px}.external-link{color:var(--orange);text-decoration:none;font-weight:600}.external-link:hover{text-decoration:underline}.error{color:var(--red)}.wizard-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(108px,1fr));gap:var(--space-2);list-style:none;padding:0;margin:0 0 var(--space-5)}.wizard-steps li{position:relative;min-height:52px;display:grid;align-content:center;gap:2px;border:1px solid var(--hairline);border-radius:var(--radius-md);padding:8px 10px;background:var(--canvas-soft);color:var(--muted);font-size:12px;font-weight:650;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast)}.wizard-steps li[data-done=true]{border-color:color-mix(in srgb,var(--green) 35%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,var(--surface));color:var(--green)}.wizard-steps li[data-active=true]{border-color:var(--orange);background:var(--surface);color:var(--ink);box-shadow:0 0 0 3px var(--orange-soft)}.wizard-steps .step-index{font-size:10px;font-weight:700;letter-spacing:.04em;color:var(--muted-soft)}.wizard-steps li[data-active=true] .step-index,.wizard-steps li[data-done=true] .step-index{color:inherit;opacity:.8}.wizard-steps .step-label{line-height:1.25}.field{display:grid;gap:var(--space-1);margin:0 0 var(--space-4);width:min(100%,520px)}.field span{font-size:12px;font-weight:600;color:var(--muted)}.field input,.field textarea,.field select{width:100%;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.field input{min-height:40px;padding:0 var(--space-3)}.field select{min-height:40px;padding:0 var(--space-3);cursor:pointer}.field textarea{min-height:120px;padding:var(--space-3);resize:vertical}.field input:focus-visible,.field textarea:focus-visible,.field select:focus-visible{outline:none;border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.field input:disabled,.field textarea:disabled,.field select:disabled{background:var(--surface-muted);color:var(--muted);cursor:not-allowed}.workflow-hint{margin:calc(var(--space-1) * -1) 0 var(--space-3);color:var(--muted);font-size:12px}.meta-row{display:grid;gap:6px;margin:0 0 var(--space-4);padding:var(--space-3) var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--canvas-soft);font-size:13px}.meta-row code{font-size:12px}.cta-row,.recovery-cta-row{display:flex;flex-wrap:wrap;gap:var(--space-2);align-items:center}.cta-row{margin-top:var(--space-3)}.cta-row .cmd{flex:1 1 100%}.wizard-body{display:grid;gap:var(--space-3);justify-items:stretch;animation:panel-in .24s ease}.wizard-body>div{display:grid;gap:var(--space-3);justify-items:stretch}.wizard-body .field{width:100%;max-width:640px}.wizard-body .cta-row{margin-top:var(--space-1)}.interview-card{width:100%;padding:var(--space-5);border:1px solid var(--hairline);border-radius:var(--radius-lg);background:linear-gradient(180deg,var(--surface),var(--canvas-soft));box-shadow:var(--shadow-panel)}.interview-q{margin:0 0 var(--space-2);color:var(--ink)}.btn,button{appearance:none;display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:0 16px;border:1px solid var(--hairline-strong);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);font-size:13px;font-weight:650;cursor:pointer;transition:border-color var(--transition-fast),background-color var(--transition-fast),color var(--transition-fast),box-shadow var(--transition-fast),transform var(--transition-fast)}.btn:hover:not(:disabled),button:hover:not(:disabled){border-color:color-mix(in srgb,var(--ink) 18%,var(--hairline-strong));background:var(--canvas-soft);outline:none}.btn:focus-visible:not(:disabled),button:focus-visible:not(:disabled){outline:none;box-shadow:0 0 0 3px var(--orange-soft)}.btn:active:not(:disabled),button:active:not(:disabled){transform:translateY(1px)}.btn:disabled,button:disabled{opacity:.48;cursor:not-allowed;transform:none}.btn-primary,button.btn-primary{border-color:var(--orange);background:var(--orange);color:#fff;box-shadow:0 6px 16px #f54e002e}.btn-primary:hover:not(:disabled),button.btn-primary:hover:not(:disabled){border-color:var(--orange-active);background:var(--orange-active);color:#fff}.btn-secondary,button.btn-secondary{border-color:var(--hairline-strong);background:var(--surface);color:var(--ink)}.btn-ghost,button.btn-ghost{border-color:transparent;background:transparent;color:var(--body)}.btn-ghost:hover:not(:disabled),button.btn-ghost:hover:not(:disabled){background:var(--surface-muted);color:var(--ink)}.recovery-cta-row{margin-top:var(--space-3)}.recovery-confirm{margin-top:var(--space-3);padding:var(--space-4);border:1px solid color-mix(in srgb,var(--orange) 40%,var(--hairline));border-radius:var(--radius-md);background:color-mix(in srgb,var(--orange-soft) 55%,var(--surface))}.recovery-confirm h3{margin:0 0 var(--space-2);font-size:15px}.recovery-confirm .cta-row{margin-top:var(--space-3)}.recovery-report{margin-top:var(--space-3);padding:var(--space-4);border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface)}.recovery-report-header{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);margin-bottom:var(--space-3)}.recovery-report-header h3{margin:0;font-size:15px}.recovery-report-facts{display:grid;gap:var(--space-2);margin:0 0 var(--space-3)}.recovery-report-facts div{display:grid;grid-template-columns:5.5rem 1fr;gap:var(--space-2)}.recovery-report-facts dt{color:var(--muted);font-size:12px}.recovery-report-facts dd{margin:0;overflow-wrap:anywhere}.recovery-steps{margin:.5rem 0 .75rem 1.25rem;padding:0;line-height:1.5}.recovery-lane{margin-top:var(--space-4);padding-top:var(--space-3);border-top:1px solid var(--border, #ddd)}.recovery-lane h3{margin:0 0 .35rem;font-size:1rem}.recovery-cta-row button[disabled]{opacity:.4}@keyframes flash-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}@keyframes panel-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@media(max-width:800px){.console-shell{width:min(100% - 24px,1120px);padding-top:var(--space-5)}.console-header{grid-template-columns:1fr}.nav-link-external{margin-left:0}.inspect-split-panel{inset:auto 0 0;width:100%;max-height:min(76vh,680px);border-radius:var(--radius-lg) var(--radius-lg) 0 0}.kv div{grid-template-columns:1fr}.wizard-steps{grid-template-columns:repeat(2,minmax(0,1fr))}}.oc-shell{display:flex;flex-direction:column;height:100%;min-height:0;gap:var(--space-3);position:relative}.oc-workspace{display:flex;flex:1 1 auto;min-height:0;gap:0;position:relative}.oc-main{display:flex;flex-direction:column;flex:1 1 auto;min-width:0;min-height:0}.oc-process{display:flex;flex-direction:column;flex:0 0 min(360px,42%);width:min(360px,42%);min-width:280px;min-height:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-panel);margin-left:var(--space-3);overflow:hidden;animation:panel-in .18s ease}.oc-process-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-process-title{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:13px;color:var(--ink)}.oc-process-title-mark{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-process-count{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:18px;padding:0 6px;border-radius:var(--radius-pill);background:var(--surface-muted);color:var(--muted);font-size:11px;font-weight:650}.oc-process-close{font-size:12px}.oc-process-timeline{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:0}.oc-process-empty{padding:var(--space-4);color:var(--muted);font-size:12.5px;line-height:1.5;text-align:center}.oc-process-item{display:grid;grid-template-columns:20px 52px minmax(0,1fr);gap:8px;align-items:start;padding:4px 0}.oc-process-rail{position:relative;display:grid;place-items:center;min-height:34px}.oc-process-rail:before{position:absolute;top:0;bottom:0;width:1px;background:var(--hairline);content:""}.oc-process-item.is-first .oc-process-rail:before{top:50%}.oc-process-item.is-last .oc-process-rail:before{bottom:50%}.oc-process-dot{position:relative;z-index:1;width:8px;height:8px;border-radius:50%;background:var(--surface);border:2px solid color-mix(in srgb,var(--orange) 55%,var(--hairline))}.oc-process-item.is-running .oc-process-dot{border-color:var(--orange-active);background:var(--orange-soft)}.oc-process-item.is-error .oc-process-dot{border-color:var(--red);background:color-mix(in srgb,var(--red) 18%,var(--surface))}.oc-process-time{padding-top:10px;font-size:11px;color:var(--muted-soft);font-variant-numeric:tabular-nums;white-space:nowrap}.oc-process-body{min-width:0}.oc-process-toggle{cursor:pointer;font:inherit}.oc-process-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-process-toggle.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));color:var(--orange-active)}.oc-process-toggle.is-error{border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface));color:var(--red)}.oc-process-summary{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid var(--hairline);border-radius:var(--radius-pill);background:var(--surface);color:var(--muted);font-size:11.5px;font-weight:650;cursor:pointer;font:inherit;max-width:min(420px,100%);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.oc-process-summary:hover,.oc-process-summary.is-open{border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));color:var(--orange-active)}.oc-process-summary.is-running{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:var(--orange-soft)}.oc-process-summary.is-error{color:var(--red);border-color:color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 8%,var(--surface))}.oc-process-summary.is-ok{color:var(--green);border-color:color-mix(in srgb,var(--green) 30%,var(--hairline))}@media(max-width:900px){.oc-workspace.has-process .oc-process{position:absolute;top:0;right:0;bottom:0;z-index:8;width:min(360px,calc(100% - 24px));min-width:0;flex:none;margin-left:0;border-radius:var(--radius-md) 0 0 var(--radius-md);box-shadow:var(--shadow-soft)}}.oc-banner{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2) var(--space-3);padding:10px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:color-mix(in srgb,var(--surface) 94%,transparent);box-shadow:var(--shadow-panel)}.oc-banner-title{display:inline-flex;align-items:center;gap:6px;font-weight:700;color:var(--ink)}.oc-banner-dot{width:8px;height:8px;border-radius:50%;background:var(--orange)}.oc-banner-body{color:var(--body);font-size:12.5px;line-height:1.45}.oc-banner-body strong{color:var(--ink)}.oc-cap-chips{display:inline-flex;flex-wrap:wrap;gap:6px;margin-left:auto;align-items:center}.oc-cap-chip{display:inline-flex;align-items:center;gap:5px;padding:3px 10px;border:1px solid color-mix(in srgb,var(--green) 30%,var(--hairline));border-radius:var(--radius-pill);background:color-mix(in srgb,var(--mint) 50%,var(--surface));color:var(--green);font-size:11.5px;font-weight:650}button.oc-cap-chip{font:inherit;cursor:pointer}.oc-tools-menu{position:relative}.oc-tools-toggle.is-active{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline));background:color-mix(in srgb,var(--orange) 10%,var(--surface));color:var(--orange-active)}.oc-tools-catalog{position:absolute;top:calc(100% + 8px);right:0;z-index:20;width:min(420px,calc(100vw - 48px));max-height:min(480px,60vh);display:flex;flex-direction:column;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);box-shadow:var(--shadow-soft);animation:panel-in .16s ease;overflow:hidden}.oc-tools-catalog-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--hairline);flex-shrink:0}.oc-tools-catalog-head strong{font-size:13px;color:var(--ink)}.oc-tools-catalog-meta{font-size:11.5px;color:var(--muted-soft)}.oc-tools-catalog-close{margin-left:auto;font-size:12px}.oc-tools-catalog-body{overflow-y:auto;padding:var(--space-3);display:flex;flex-direction:column;gap:var(--space-3)}.oc-tools-group-title{margin:0 0 8px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--muted-soft)}.oc-tools-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.oc-tools-item{padding:8px 10px;border:1px solid var(--hairline);border-radius:var(--radius-sm);background:color-mix(in srgb,var(--surface-muted) 55%,var(--surface))}.oc-tools-item-head{display:flex;align-items:center;gap:8px;margin-bottom:4px}.oc-tools-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;color:var(--ink)}.oc-tools-kind{margin-left:auto;font-size:10.5px;font-weight:650;padding:1px 7px;border-radius:var(--radius-pill);color:var(--muted);background:var(--surface);border:1px solid var(--hairline)}.oc-tools-kind.is-explore,.oc-tools-kind.is-read{color:var(--green);border-color:color-mix(in srgb,var(--green) 28%,var(--hairline));background:color-mix(in srgb,var(--mint) 45%,transparent)}.oc-tools-kind.is-mutation{color:var(--orange-active);border-color:color-mix(in srgb,var(--orange) 35%,var(--hairline));background:var(--orange-soft)}.oc-tools-kind.is-long-running{color:var(--amber);border-color:color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 10%,var(--surface))}.oc-tools-desc{margin:0;font-size:12px;line-height:1.45;color:var(--body)}.oc-tools-cli{margin:4px 0 0;font-size:11px;color:var(--muted-soft)}.oc-tools-cli code{font-size:11px;color:var(--muted)}.oc-alert{padding:8px 12px;border-radius:var(--radius-md);font-size:12.5px;line-height:1.45;animation:panel-in .18s ease}.oc-alert-warning{border:1px solid color-mix(in srgb,var(--amber) 35%,var(--hairline));background:color-mix(in srgb,var(--amber) 8%,var(--surface));color:var(--amber)}.oc-alert-error{border:1px solid color-mix(in srgb,var(--red) 35%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-messages{flex:1 1 auto;min-height:0;overflow-y:auto;padding:var(--space-4) var(--space-3);display:flex;flex-direction:column;gap:var(--space-4)}.oc-day-sep{display:flex;align-items:center;gap:var(--space-3);color:var(--muted-soft);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;margin:var(--space-2) 0}.oc-day-sep:before,.oc-day-sep:after{content:"";flex:1;height:1px;background:var(--hairline)}.oc-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:var(--space-4);text-align:center;padding:var(--space-6) var(--space-4)}.oc-empty-icon{display:grid;place-items:center;width:64px;height:64px;border-radius:50%;background:var(--orange-soft);color:var(--orange-active);font-size:30px;font-weight:800;font-family:var(--font-mono, "JetBrains Mono", monospace)}.oc-empty h3{margin:0;font-size:18px;color:var(--ink)}.oc-empty p{margin:0;max-width:46ch;color:var(--muted);font-size:13px;line-height:1.5}.oc-suggestions{display:grid;grid-template-columns:1fr 1fr;gap:var(--space-2);width:100%;max-width:640px;margin-top:var(--space-2)}@media(max-width:560px){.oc-suggestions{grid-template-columns:1fr}}.oc-suggestion{appearance:none;display:flex;flex-direction:column;gap:4px;text-align:left;padding:12px 14px;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);color:var(--ink);cursor:pointer;transition:border-color var(--transition-fast),background var(--transition-fast),transform var(--transition-fast),box-shadow var(--transition-fast)}.oc-suggestion:hover{border-color:var(--orange);background:var(--surface);box-shadow:0 0 0 3px var(--orange-soft);transform:translateY(-1px)}.oc-suggestion strong{font-size:13px;font-weight:700}.oc-suggestion span{color:var(--muted);font-size:12px}.oc-row{display:flex;flex-direction:column;gap:4px;max-width:min(100%,900px)}.oc-row.is-user{align-self:flex-end;align-items:flex-end}.oc-row.is-assistant,.oc-row.is-tool{align-self:flex-start;align-items:stretch;width:min(100%,760px)}.oc-meta{display:flex;align-items:center;gap:8px;font-size:11.5px;color:var(--muted-soft)}.oc-avatar{display:grid;place-items:center;width:22px;height:22px;border-radius:50%;font-size:11px;font-weight:700;flex-shrink:0}.oc-avatar-user{background:var(--ink);color:#fff}.oc-avatar-assistant{background:var(--orange);color:#fff}.oc-role{font-weight:700;color:var(--body)}.oc-time{font-variant-numeric:tabular-nums}.oc-bubble{padding:10px 14px;border-radius:var(--radius-lg);font-size:14px;line-height:1.6;overflow-wrap:anywhere;word-break:break-word;animation:bubble-in .16s ease}@keyframes bubble-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}.oc-bubble-user{background:var(--ink);color:#fff;border-bottom-right-radius:4px}.oc-bubble-assistant{background:var(--surface);color:var(--ink);border:1px solid var(--hairline);border-bottom-left-radius:4px;box-shadow:var(--shadow-panel)}.oc-bubble-tool{background:var(--canvas-soft);color:var(--body);border:1px solid var(--hairline)}.oc-bubble-error{border-color:color-mix(in srgb,var(--red) 40%,var(--hairline));background:color-mix(in srgb,var(--red) 7%,var(--surface));color:var(--red)}.oc-md{display:flex;flex-direction:column;gap:var(--space-2)}.oc-md>*:first-child{margin-top:0}.oc-md>*:last-child{margin-bottom:0}.oc-md p{margin:0}.oc-md h1,.oc-md h2,.oc-md h3,.oc-md h4{margin:var(--space-3) 0 var(--space-2);color:inherit;font-weight:700;line-height:1.3}.oc-md h1{font-size:1.3em}.oc-md h2{font-size:1.18em}.oc-md h3{font-size:1.06em}.oc-md h4{font-size:1em}.oc-md ul,.oc-md ol{margin:0;padding-left:1.4em;display:flex;flex-direction:column;gap:2px}.oc-md li{margin:0}.oc-md blockquote{margin:0;padding:2px 0 2px 12px;border-left:3px solid var(--hairline-strong);color:var(--body)}.oc-bubble-user .oc-md blockquote{border-left-color:#ffffff59;color:#ffffffd1}.oc-md code{font-size:.86em;padding:1px 5px;border-radius:4px;background:color-mix(in srgb,var(--ink) 8%,transparent)}.oc-bubble-user .oc-md code{background:#ffffff29}.oc-md pre{margin:0;padding:0;background:transparent}.oc-codeblock{margin:0;border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface-muted);overflow:hidden}.oc-bubble-assistant .oc-codeblock{background:#f3efe8}.oc-codeblock-head{display:flex;align-items:center;justify-content:space-between;gap:var(--space-2);padding:5px 6px 5px 10px;border-bottom:1px solid var(--hairline);font-size:11px;color:var(--muted);background:color-mix(in srgb,var(--ink) 4%,transparent)}.oc-codeblock-lang{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-weight:600;text-transform:lowercase}.oc-codeblock-copy{appearance:none;display:inline-flex;align-items:center;gap:5px;min-height:26px;padding:0 9px;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:var(--muted);font-size:11px;font-weight:600;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast),border-color var(--transition-fast)}.oc-codeblock-copy:hover{background:var(--surface);color:var(--ink);border-color:var(--hairline-strong)}.oc-codeblock pre{margin:0;padding:var(--space-3) var(--space-4);overflow-x:auto;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;line-height:1.55;white-space:pre;color:var(--ink)}.oc-md a{color:var(--orange);text-decoration:none;border-bottom:1px solid color-mix(in srgb,var(--orange) 40%,transparent)}.oc-md a:hover{border-bottom-color:var(--orange)}.oc-md hr{margin:var(--space-2) 0;border:none;border-top:1px solid var(--hairline-strong)}.oc-stream{white-space:pre-wrap;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:13px;line-height:1.55}.oc-cursor{display:inline-block;width:7px;height:1.05em;margin-left:2px;vertical-align:text-bottom;background:var(--orange);animation:oc-blink 1s steps(2,start) infinite;border-radius:1px}@keyframes oc-blink{to{opacity:0}}.oc-tool{border:1px solid var(--hairline);border-radius:var(--radius-md);background:var(--surface);overflow:hidden;box-shadow:var(--shadow-panel);animation:panel-in .16s ease}.oc-tool.is-error{border-color:color-mix(in srgb,var(--red) 38%,var(--hairline))}.oc-tool.is-running{border-color:color-mix(in srgb,var(--orange) 40%,var(--hairline))}.oc-tool-head{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:9px 12px;border:none;background:transparent;text-align:left;cursor:pointer;font:inherit;color:var(--ink)}.oc-tool-head:hover{background:var(--surface-muted)}.oc-tool-chevron{flex-shrink:0;width:12px;height:12px;transition:transform var(--transition-fast)}.oc-tool-head[aria-expanded=true] .oc-tool-chevron{transform:rotate(90deg)}.oc-tool-icon{display:grid;place-items:center;width:22px;height:22px;border-radius:6px;background:var(--surface-muted);color:var(--orange-active);flex-shrink:0}.oc-tool-name{font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12.5px;font-weight:650;flex-shrink:0}.oc-tool-args{color:var(--muted);font-size:11.5px;font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-right:8px;min-width:0}.oc-tool-status{margin-left:auto;flex-shrink:0;font-size:11px;font-weight:650;padding:2px 8px;border-radius:var(--radius-pill)}.oc-tool-waiting{color:var(--muted);font-size:12px;animation:pulse 1.5s infinite}.oc-tool-status.is-running{color:var(--orange-active);background:var(--orange-soft)}.oc-tool-status.is-ok{color:var(--green);background:color-mix(in srgb,var(--mint) 60%,transparent)}.oc-tool-status.is-error{color:var(--red);background:color-mix(in srgb,var(--red) 10%,transparent)}.oc-tool-body{padding:0 var(--space-4) var(--space-3);border-top:1px solid var(--hairline)}.oc-tool-section{display:flex;flex-direction:column;gap:4px;padding-top:var(--space-3)}.oc-tool-label{font-size:11px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--muted-soft)}.oc-tool-code{margin:0;padding:var(--space-3);border-radius:var(--radius-sm);background:var(--surface-muted);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word;max-height:260px;overflow-y:auto;color:var(--body)}.oc-composer{flex-shrink:0;border-top:1px solid var(--hairline);padding:var(--space-3) var(--space-4) var(--space-4);background:color-mix(in srgb,var(--canvas) 86%,transparent)}.oc-composer-inner{display:flex;flex-direction:column;gap:var(--space-2);max-width:1080px;margin:0 auto}.oc-composer-shell{position:relative;display:flex;align-items:flex-end;gap:var(--space-2);padding:8px 8px 8px 14px;border:1px solid var(--hairline-strong);border-radius:var(--radius-lg);background:var(--surface);box-shadow:var(--shadow-panel);transition:border-color var(--transition-fast),box-shadow var(--transition-fast)}.oc-composer-shell:focus-within{border-color:var(--orange);box-shadow:0 0 0 3px var(--orange-soft)}.oc-composer-shell.is-busy{opacity:.78}.oc-textarea{flex:1 1 auto;min-width:0;min-height:52px;max-height:280px;padding:8px 0;border:none;background:transparent;color:var(--ink);font:inherit;font-size:14px;line-height:1.5;resize:none;outline:none}.oc-textarea::placeholder{color:var(--muted-soft)}.oc-send{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--orange);border-radius:50%;background:var(--orange);color:#fff;cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast),transform var(--transition-fast)}.oc-send:hover:not(:disabled){background:var(--orange-active);border-color:var(--orange-active)}.oc-send:active:not(:disabled){transform:scale(.94)}.oc-send:disabled{opacity:.4;cursor:not-allowed}.oc-stop{appearance:none;display:grid;place-items:center;width:38px;height:38px;flex-shrink:0;padding:0;border:1px solid var(--hairline-strong);border-radius:50%;background:var(--surface);color:var(--red);cursor:pointer;transition:background var(--transition-fast),border-color var(--transition-fast)}.oc-stop:hover{background:color-mix(in srgb,var(--red) 8%,var(--surface));border-color:var(--red)}.oc-composer-foot{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap;font-size:11.5px;color:var(--muted)}.oc-composer-hint{display:inline-flex;align-items:center;gap:5px}.oc-kbd{display:inline-block;padding:1px 6px;border:1px solid var(--hairline-strong);border-bottom-width:2px;border-radius:4px;background:var(--surface);color:var(--body);font-family:JetBrains Mono,SFMono-Regular,Consolas,monospace;font-size:10.5px;line-height:1.4}.oc-session-info{display:inline-flex;align-items:center;gap:8px;margin-left:auto;font-size:11px;color:var(--muted-soft)}.oc-session-info code{font-size:11px;color:var(--muted)}.oc-new-session{margin-left:auto}@media(max-width:600px){.oc-session-info{width:100%;margin-left:0}}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Loop 操作台 · Operator Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DVl7Jxt5.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-lVcIr9Ju.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
* `fullstack-v1` is NOT a new DAG or taskKind. It is `agent-worker`'s
|
|
6
6
|
* deterministic structural validation + read-model projection policy over a
|
|
7
7
|
* Feature Packet. See
|
|
8
|
-
* `docs/design/agent-worker-fullstack-workflow-integration.md` §9.
|
|
8
|
+
* `docs/design/active/agent-worker-fullstack-workflow-integration.md` §9.
|
|
9
9
|
*
|
|
10
10
|
* A Feature Packet declares its profile via an optional `feature.yaml` next to
|
|
11
11
|
* `acceptance.yaml`. A packet without `feature.yaml` (or one that does not set
|
|
@@ -8,6 +8,7 @@ import { getTaskPoolRoot, listLegacyStateFiles, listTaskPoolStates, } from "../p
|
|
|
8
8
|
import { assessDagRunLiveness, assessDagRunRecoveryEligibility, deriveDagRunEffectiveStatus, } from "../../workflows/dag/lifecycle.js";
|
|
9
9
|
import { parseDagSpec, resolveModelForTask, } from "../../workflows/dag/types.js";
|
|
10
10
|
import { loadFeatureDecisionModels } from "../feature/decision-loader.js";
|
|
11
|
+
import { frontendLintAssessmentArtifactSchema } from "../../workflows/dag/frontend-lint-baseline.js";
|
|
11
12
|
/** Internal map key: featureId + NUL + taskId (stable, non-printable delimiter). */
|
|
12
13
|
export function taskIdentityKey(featureId, taskId) {
|
|
13
14
|
return `${featureId}\0${taskId}`;
|
|
@@ -1525,6 +1526,21 @@ async function loadBackendTestProjection(runDir, nodes) {
|
|
|
1525
1526
|
...(manifest && manifest.coverageSummary ? { coverage: manifest.coverageSummary } : {}),
|
|
1526
1527
|
};
|
|
1527
1528
|
}
|
|
1529
|
+
async function loadFrontendLintProjection(runDir) {
|
|
1530
|
+
const decoded = await safeReadJson(path.join(runDir, "contracts", "frontend-lint-assessment.json"));
|
|
1531
|
+
if (!decoded)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
const parsed = frontendLintAssessmentArtifactSchema.safeParse(decoded);
|
|
1534
|
+
if (!parsed.success)
|
|
1535
|
+
return undefined;
|
|
1536
|
+
return {
|
|
1537
|
+
status: parsed.data.status,
|
|
1538
|
+
writerChangedFiles: parsed.data.writerChangedFiles,
|
|
1539
|
+
toleratedDiagnosticCount: parsed.data.toleratedDiagnosticCount,
|
|
1540
|
+
blockingDiagnosticCount: parsed.data.blockingDiagnostics.length,
|
|
1541
|
+
blockingReasons: parsed.data.blockingReasons,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1528
1544
|
async function parseDagStateFile(statePath, now) {
|
|
1529
1545
|
try {
|
|
1530
1546
|
if (!existsSync(statePath))
|
|
@@ -1579,7 +1595,10 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1579
1595
|
liveness: liveness.status,
|
|
1580
1596
|
})
|
|
1581
1597
|
: undefined;
|
|
1582
|
-
const backendTest = await
|
|
1598
|
+
const [backendTest, frontendLint] = await Promise.all([
|
|
1599
|
+
loadBackendTestProjection(runDir, nodes),
|
|
1600
|
+
loadFrontendLintProjection(runDir),
|
|
1601
|
+
]);
|
|
1583
1602
|
const continuationRecord = state.continuation &&
|
|
1584
1603
|
typeof state.continuation.parentRunId === "string" &&
|
|
1585
1604
|
typeof state.continuation.effectiveFromNodeId === "string" &&
|
|
@@ -1631,6 +1650,7 @@ async function parseDagStateFile(statePath, now) {
|
|
|
1631
1650
|
dagPath: runDir,
|
|
1632
1651
|
...(continuation ? { continuation } : {}),
|
|
1633
1652
|
...(backendTest ? { backendTest } : {}),
|
|
1653
|
+
...(frontendLint ? { frontendLint } : {}),
|
|
1634
1654
|
};
|
|
1635
1655
|
}
|
|
1636
1656
|
catch {
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
5
|
+
import { isOpenspecSpecFilePath, isOpenspecSpecPath, isOpenspecSpecSearchTarget, } from "../../shared/openspec-spec.js";
|
|
5
6
|
const DAG_RUN_LIFECYCLE_DIRS = ["active", "completed", "paused"];
|
|
6
7
|
/**
|
|
7
8
|
* Known knowledge-base connector tool names.
|
|
@@ -18,7 +19,8 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
18
19
|
]);
|
|
19
20
|
/**
|
|
20
21
|
* Pattern for detecting spec-related files:
|
|
21
|
-
* - openspec
|
|
22
|
+
* - openspec files (generic evidence, including non-normative paths)
|
|
23
|
+
* - ai_workspace files
|
|
22
24
|
* - *.spec.md / *.spec.ts / *.spec.tsx
|
|
23
25
|
* - project-specs/**
|
|
24
26
|
* - design-spec.md, code-standards.md, review-checklist.md, etc.
|
|
@@ -27,6 +29,7 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
27
29
|
*/
|
|
28
30
|
const SPEC_FILE_PATTERNS = [
|
|
29
31
|
/openspec\//i,
|
|
32
|
+
/ai_workspace\//i,
|
|
30
33
|
/\/project-specs\//i,
|
|
31
34
|
/\/spec\//i,
|
|
32
35
|
/\.spec\.(md|tsx?|jsx?)$/i,
|
|
@@ -47,22 +50,16 @@ function isSpecFilePath(filePath) {
|
|
|
47
50
|
function isKnowledgeBaseTool(toolName) {
|
|
48
51
|
return KB_CONNECTOR_TOOLS.has(toolName);
|
|
49
52
|
}
|
|
50
|
-
/** A repo-relative path references
|
|
53
|
+
/** A repo-relative path references a canonical openspec spec directory. */
|
|
51
54
|
function isOpenspecPath(filePath) {
|
|
52
|
-
|
|
53
|
-
return normalized === "openspec" || normalized.startsWith("openspec/");
|
|
55
|
+
return isOpenspecSpecFilePath(filePath.replaceAll(path.sep, "/"));
|
|
54
56
|
}
|
|
55
|
-
/** A search query targets
|
|
57
|
+
/** A search query targets a canonical openspec spec directory. */
|
|
56
58
|
function isOpenspecSearch(query, searchPath) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
lower === "openspec" ||
|
|
62
|
-
lower === "openspec/" ||
|
|
63
|
-
lower.startsWith("openspec/") ||
|
|
64
|
-
lower.includes("openspec/**") ||
|
|
65
|
-
lower.includes("openspec/*"));
|
|
59
|
+
const normalizedPath = searchPath?.replaceAll(path.sep, "/");
|
|
60
|
+
if (normalizedPath && isOpenspecSpecPath(normalizedPath))
|
|
61
|
+
return true;
|
|
62
|
+
return isOpenspecSpecSearchTarget(query);
|
|
66
63
|
}
|
|
67
64
|
function resolveSessionEventsPath(repoRoot, dagRunId, nodeId) {
|
|
68
65
|
if (!isSafeObservabilityIdentifier(dagRunId) || !isSafeObservabilityIdentifier(nodeId)) {
|
|
@@ -362,7 +359,7 @@ export async function extractSpecEvidence(repoRoot, dagRunId, nodeId) {
|
|
|
362
359
|
if (status === "no-evidence") {
|
|
363
360
|
summaryLines.push("未观察到任何规范证据:无 skill 注入、无文件读取、无检索操作。");
|
|
364
361
|
}
|
|
365
|
-
// Separate openspec
|
|
362
|
+
// Separate openspec spec directory reads and searches for Dashboard display.
|
|
366
363
|
// Knowledge base and openspec are parallel sources.
|
|
367
364
|
const openspecReads = specReads.filter((r) => isOpenspecPath(r.path));
|
|
368
365
|
const openspecSearches = searches.filter((s) => isOpenspecSearch(s.query, s.path));
|
|
@@ -46,6 +46,28 @@ export function getActiveNodes(dag) {
|
|
|
46
46
|
return (dag.nodes ?? []).filter((n) => isNodeActive(n.status));
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function frontendLintSummary(frontendLint) {
|
|
50
|
+
if (!frontendLint) return null;
|
|
51
|
+
const statusLabels = {
|
|
52
|
+
passed: "passed",
|
|
53
|
+
"baseline-debt": "baseline-debt",
|
|
54
|
+
failed: "failed",
|
|
55
|
+
unavailable: "unavailable",
|
|
56
|
+
};
|
|
57
|
+
const status = statusLabels[frontendLint.status] ?? "unavailable";
|
|
58
|
+
const changed = frontendLint.writerChangedFiles?.length ?? 0;
|
|
59
|
+
const tolerated = frontendLint.toleratedDiagnosticCount ?? 0;
|
|
60
|
+
const blocked = frontendLint.blockingDiagnosticCount ?? 0;
|
|
61
|
+
const reason = (frontendLint.blockingReasons ?? []).join(";");
|
|
62
|
+
return [
|
|
63
|
+
status,
|
|
64
|
+
`修改文件 ${changed}`,
|
|
65
|
+
`容忍存量诊断 ${tolerated}`,
|
|
66
|
+
`阻断诊断 ${blocked}`,
|
|
67
|
+
...(reason ? [`原因:${reason}`] : []),
|
|
68
|
+
].join(" · ");
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
export function dagSortTime(dag) {
|
|
50
72
|
return dag.startedAt ?? dag.finishedAt ?? "";
|
|
51
73
|
}
|
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from "../shell-chrome.js";
|
|
80
80
|
import {
|
|
81
81
|
dagProgress,
|
|
82
|
+
frontendLintSummary,
|
|
82
83
|
isNodeFailed,
|
|
83
84
|
isNodeFinished,
|
|
84
85
|
isNodeActive,
|
|
@@ -223,6 +224,10 @@ export async function renderDagDetail(dagRunId, initial = true) {
|
|
|
223
224
|
["成功/失败/跳过", `${succeededCount}/${failedCount}/${skippedCount}`],
|
|
224
225
|
["DAG 路径", dag.dagPath ?? "—"],
|
|
225
226
|
);
|
|
227
|
+
const lintSummary = frontendLintSummary(dag.frontendLint);
|
|
228
|
+
if (lintSummary) {
|
|
229
|
+
primaryEntries.push(["前端 lint 判定", lintSummary]);
|
|
230
|
+
}
|
|
226
231
|
const primaryGrid = metaGrid(primaryEntries);
|
|
227
232
|
const diagnosticGrid = metaGrid([
|
|
228
233
|
["当前判定", dagStatusBadge(dag.effectiveStatus ?? dag.status)],
|
|
@@ -763,7 +763,44 @@ function advisoryBadge(status) {
|
|
|
763
763
|
const className = status === "PASS" ? "passed" : status === "FAIL" ? "failure" : "skipped";
|
|
764
764
|
return `<span class="status ${className}">${status === "Unavailable" ? "不可用" : status}</span>`;
|
|
765
765
|
}
|
|
766
|
+
function conclusionIcon(failed) {
|
|
767
|
+
return failed
|
|
768
|
+
? `<svg class="conclusion-icon" viewBox="0 0 48 48" role="img" aria-label="测试未通过"><circle cx="24" cy="24" r="20"/><path d="M24 14v13"/><circle cx="24" cy="34" r="1.5" class="icon-dot"/></svg>`
|
|
769
|
+
: `<svg class="conclusion-icon" viewBox="0 0 48 48" role="img" aria-label="测试通过"><circle cx="24" cy="24" r="20"/><path d="m14 24 7 7 14-15"/></svg>`;
|
|
770
|
+
}
|
|
771
|
+
function reportDetailSection(title, content, open = false) {
|
|
772
|
+
return `<details class="report-detail"${open ? " open" : ""}><summary>${escapeHtml(title)}</summary><div class="report-detail-body">${content}</div></details>`;
|
|
773
|
+
}
|
|
766
774
|
export function renderBackendTestHtml(input) {
|
|
775
|
+
const failed = input.parsed.failed + input.parsed.errors > 0;
|
|
776
|
+
const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
|
|
777
|
+
const catalog = new Map((input.cases ?? []).map((item) => [item.id, item]));
|
|
778
|
+
const caseValidation = parseAdvisorySummary(input.caseValidationSummary);
|
|
779
|
+
const traceability = parseAdvisorySummary(input.traceabilitySummary);
|
|
780
|
+
const failures = input.parsed.cases.filter((result) => result.status !== "passed");
|
|
781
|
+
const rows = input.parsed.cases.map((result) => {
|
|
782
|
+
const caseId = junitCaseId(result.name) ?? "未关联";
|
|
783
|
+
const item = catalog.get(caseId);
|
|
784
|
+
const io = requestResponseSummary(result);
|
|
785
|
+
const failureDetails = result.status === "passed"
|
|
786
|
+
? ""
|
|
787
|
+
: `<div class="failure-detail"><strong>${escapeHtml(result.message || humanStatus(result.status))}</strong>${result.details ? reportDetailSection("失败详情", `<pre>${escapeHtml(result.details)}</pre>`) : ""}</div>`;
|
|
788
|
+
return `<article class="case-card result-${result.status}"><div class="case-head"><div><span class="case-id">${escapeHtml(caseId)}</span><h3>${escapeHtml(item?.title ?? result.name)}</h3><p>${escapeHtml(item?.scenario ?? "未从 Markdown 用例提取场景说明。")}</p></div><div class="case-outcome"><span class="status ${result.status}">${humanStatus(result.status)}</span><span class="duration">${formatDuration(result.durationMs)}</span></div></div><div class="case-meta"><span>自动化用例</span><code>${escapeHtml(result.name)}</code></div><div class="io-grid">${reportDetails("接口请求参数", io.requests, "本次 JUnit 未捕获请求日志。")}${reportDetails("接口响应结果", io.responses, "本次 JUnit 未捕获响应日志。")}</div>${failureDetails}</article>`;
|
|
789
|
+
}).join("");
|
|
790
|
+
const failureOverview = failures.length > 0
|
|
791
|
+
? `<div class="failure-list">${failures.map((result) => {
|
|
792
|
+
const caseId = junitCaseId(result.name) ?? "未关联";
|
|
793
|
+
return `<article><div>${advisoryBadge(result.status === "skipped" ? "Unavailable" : "FAIL")}<strong>${escapeHtml(caseId)} · ${escapeHtml(catalog.get(caseId)?.title ?? result.name)}</strong></div><p>${escapeHtml(result.message || humanStatus(result.status))}</p></article>`;
|
|
794
|
+
}).join("")}</div>`
|
|
795
|
+
: '<div class="empty-state">本轮没有失败、错误或跳过用例。</div>';
|
|
796
|
+
const conclusion = failed
|
|
797
|
+
? `本轮测试未通过:${input.parsed.failed + input.parsed.errors} 条用例失败或错误。`
|
|
798
|
+
: `本轮测试通过:${input.parsed.passed} 条用例执行成功。`;
|
|
799
|
+
const executionOverview = `<div class="metric-grid"><div><span>用例总数</span><strong>${input.parsed.tests}</strong></div><div><span>通过</span><strong>${input.parsed.passed}</strong></div><div><span>失败 / 错误</span><strong>${input.parsed.failed} / ${input.parsed.errors}</strong></div><div><span>跳过</span><strong>${input.parsed.skipped}</strong></div><div><span>通过率</span><strong>${(passRate * 100).toFixed(2)}%</strong></div><div><span>总耗时</span><strong>${formatDuration(input.parsed.durationMs)}</strong></div></div>`;
|
|
800
|
+
const qualityDetails = `<div class="quality-grid"><div class="quality-card"><header><strong>Markdown 用例校验</strong>${advisoryBadge(caseValidation.status)}</header><p>Findings:${caseValidation.findings ?? "不可用"}</p><p>${escapeHtml(caseValidation.firstFinding)}</p></div><div class="quality-card"><header><strong>Markdown → pytest 追溯</strong>${advisoryBadge(traceability.status)}</header><p>Findings:${traceability.findings ?? "不可用"}</p><p>${escapeHtml(traceability.firstFinding)}</p></div></div><div class="evidence-line"><span>执行环境</span>${escapeHtml(input.environmentSummary || "不可用")}</div>`;
|
|
801
|
+
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(input.title)}</title><style>:root{color-scheme:light;--ink:#182230;--muted:#667085;--line:#dce3ec;--panel:#f6f8fb;--brand:#163a63;--accent:#2e6da4;--pass:#067647;--pass-bg:#ecfdf3;--fail:#b42318;--fail-bg:#fef3f2;--skip:#9a6700;--skip-bg:#fffaeb;--shadow:0 18px 45px rgba(28,55,90,.09)}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at top left,#dcecff 0,transparent 32rem),linear-gradient(180deg,#edf3fa 0,#f8fafc 34rem);color:var(--ink);font-family:Inter,system-ui,"Microsoft YaHei","PingFang SC",sans-serif;line-height:1.65}header,main{max-width:1280px;margin:auto}header{padding:3.5rem 2rem 1.75rem}main{padding:0 2rem 4rem}h1{margin:.3rem 0;color:var(--brand);font-size:clamp(2rem,4vw,3rem);letter-spacing:-.03em}h2{margin:0 0 1rem;color:var(--brand)}h3{margin:.35rem 0 .15rem;font-size:1.12rem}.eyebrow{color:var(--accent);font-weight:850;letter-spacing:.12em}.subtitle,.muted,.case-head p{color:var(--muted)}section,.report-detail{background:rgba(255,255,255,.94);border:1px solid rgba(220,227,236,.95);border-radius:1.15rem;padding:1.5rem;margin-top:1rem;box-shadow:var(--shadow)}.conclusion{display:flex;align-items:center;gap:1rem;padding:1rem 1.25rem;border-left:5px solid ${failed ? "var(--fail)" : "var(--pass)"};border-radius:.8rem;background:${failed ? "var(--fail-bg)" : "var(--pass-bg)"};color:${failed ? "var(--fail)" : "var(--pass)"}}.conclusion-icon{width:4rem;height:4rem;flex:0 0 4rem;display:block;overflow:visible}.conclusion-icon circle:first-child{fill:currentColor;fill-opacity:.12;stroke:currentColor;stroke-width:2}.conclusion-icon path{fill:none;stroke:currentColor;stroke-width:3.2;stroke-linecap:round;stroke-linejoin:round}.conclusion-icon .icon-dot{fill:currentColor;stroke:none}.conclusion-copy{min-width:0}.conclusion-copy strong{display:block;font-size:1.05rem}.conclusion-copy span{display:block;font-size:.9rem;font-weight:400}.metric-grid{display:grid;grid-template-columns:repeat(6,minmax(7rem,1fr));gap:.75rem;margin-top:1rem}.metric-grid>div{padding:1rem;border:1px solid var(--line);border-radius:.85rem;background:white}.metric-grid span,.metric-grid small{display:block;color:var(--muted);font-size:.88rem}.metric-grid strong{display:block;font-size:1.45rem;font-weight:700}.quality-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.quality-card{padding:1rem;border:1px solid var(--line);border-radius:.85rem;background:white}.quality-card header{display:flex;align-items:center;justify-content:space-between;padding:0;margin:0 0 .5rem}.quality-card p{margin:.25rem 0;color:var(--muted)}.status{display:inline-flex;align-items:center;padding:.18rem .62rem;border-radius:999px;font-weight:800;font-size:.86rem}.status.passed{color:var(--pass);background:var(--pass-bg)}.status.failure,.status.error{color:var(--fail);background:var(--fail-bg)}.status.skipped{color:var(--skip);background:var(--skip-bg)}.failure-list{display:grid;gap:.75rem}.failure-list article{border-left:4px solid var(--fail);background:var(--fail-bg);padding:.85rem 1rem;border-radius:.65rem}.empty-state{padding:1rem;border:1px dashed var(--line);border-radius:.75rem;color:var(--muted);background:var(--panel)}.case-list{display:grid;gap:1rem}.case-card{border:1px solid var(--line);border-radius:1rem;padding:1.15rem;background:linear-gradient(145deg,#fff,#fbfcfe);box-shadow:0 8px 24px rgba(28,55,90,.06)}.case-card.result-failure,.case-card.result-error{border-left:5px solid var(--fail)}.case-card.result-passed{border-left:5px solid var(--pass)}.case-head{display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}.case-head p{margin:.2rem 0}.case-id{display:inline-block;color:var(--accent);font-weight:850;letter-spacing:.04em}.case-outcome{display:flex;align-items:center;gap:.75rem;white-space:nowrap}.duration{color:var(--muted);font-variant-numeric:tabular-nums}.case-meta{display:flex;gap:.75rem;align-items:center;margin:.85rem 0;padding:.65rem .8rem;border-radius:.7rem;background:var(--panel);color:var(--muted)}code{color:var(--brand);font-weight:700}.io-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.8rem}.io-details{border:1px solid var(--line);border-radius:.75rem;padding:.7rem;background:white}.io-details summary,.report-detail summary{cursor:pointer;color:var(--brand);font-weight:800}.io-details summary span{float:right;color:var(--muted)}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:18rem;overflow:auto;background:#101828;color:#eef4ff;padding:.85rem;border-radius:.6rem;font-size:.82rem}.failure-detail{margin-top:.8rem;padding:.8rem;border-radius:.7rem;background:var(--fail-bg);color:var(--fail)}.report-detail{padding:0;overflow:hidden}.report-detail summary{list-style:none;padding:1rem 1.25rem}.report-detail summary::-webkit-details-marker{display:none}.report-detail summary::after{content:"+";float:right;font-size:1.2rem;font-weight:400;color:var(--muted)}.report-detail[open] summary::after{content:"−"}.report-detail-body{border-top:1px solid var(--line);padding:1.25rem}.evidence-line{display:flex;gap:.75rem;align-items:baseline;padding:.7rem .8rem;background:var(--panel);border-radius:.65rem;font-size:.9rem}.evidence-line span{color:var(--muted);min-width:8rem}.subtitle{margin:.2rem 0 0}@media(max-width:900px){.metric-grid{grid-template-columns:repeat(2,1fr)}.quality-grid,.io-grid{grid-template-columns:1fr}.case-head{display:block}.case-outcome{margin-top:.65rem}header,main{padding-left:1rem;padding-right:1rem}}@media(max-width:420px){.conclusion{align-items:flex-start}.conclusion-icon{width:3rem;height:3rem;flex-basis:3rem}.metric-grid{grid-template-columns:1fr 1fr}}</style></head><body><header><div class="eyebrow">后端自动化测试报告</div><h1>${escapeHtml(input.title)}</h1><p class="subtitle">测试结论、质量状态与每条接口用例的请求/响应事实均来自同一次 pytest 执行。</p></header><main><section><h2>当前结论</h2><div class="conclusion">${conclusionIcon(failed)}<div class="conclusion-copy"><strong>${escapeHtml(failed ? "测试未通过" : "测试通过")}</strong><span>${escapeHtml(conclusion)}</span></div></div>${executionOverview}</section>${reportDetailSection("质量校验", qualityDetails, true)}${reportDetailSection("失败概览", failureOverview, failures.length > 0)}${reportDetailSection("用例执行结果", `<div class="case-list">${rows || '<div class="empty-state">未发现可展示的 JUnit testcase。</div>'}</div>`, true)}</main></body></html>`;
|
|
802
|
+
}
|
|
803
|
+
function renderBackendTestHtmlLegacy(input) {
|
|
767
804
|
const passRate = input.parsed.tests > 0 ? input.parsed.passed / input.parsed.tests : 0;
|
|
768
805
|
const catalog = new Map((input.cases ?? []).map((item) => [item.id, item]));
|
|
769
806
|
const rows = input.parsed.cases.map((result) => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
|
+
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
9
10
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
10
11
|
/**
|
|
11
12
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
@@ -122,7 +123,7 @@ export const frontendImplementationContractSchema = z
|
|
|
122
123
|
verificationTargetIds: z.array(z.string().min(1)),
|
|
123
124
|
evidenceGap: gap.optional(),
|
|
124
125
|
})
|
|
125
|
-
.strict()),
|
|
126
|
+
.strict()).min(1),
|
|
126
127
|
uiStates: z.array(z
|
|
127
128
|
.object({
|
|
128
129
|
name: z.string().min(1),
|
|
@@ -132,7 +133,7 @@ export const frontendImplementationContractSchema = z
|
|
|
132
133
|
verificationTargetIds: z.array(z.string().min(1)).optional(),
|
|
133
134
|
notApplicableReason: z.string().min(1).optional(),
|
|
134
135
|
})
|
|
135
|
-
.strict()),
|
|
136
|
+
.strict()).min(1),
|
|
136
137
|
interactions: z.array(z
|
|
137
138
|
.object({
|
|
138
139
|
name: z.string().min(1),
|
|
@@ -185,7 +186,7 @@ export const frontendImplementationContractSchema = z
|
|
|
185
186
|
requirementIds: z.array(id),
|
|
186
187
|
uiStates: z.array(z.string().min(1)),
|
|
187
188
|
})
|
|
188
|
-
.strict()),
|
|
189
|
+
.strict()).min(1),
|
|
189
190
|
evidenceGaps: z.array(gap),
|
|
190
191
|
})
|
|
191
192
|
.strict()
|
|
@@ -198,6 +199,30 @@ export const frontendImplementationContractSchema = z
|
|
|
198
199
|
path: ["verificationTargets"],
|
|
199
200
|
});
|
|
200
201
|
const known = new Set(value.sourceBinding.requirementIds);
|
|
202
|
+
const stateNames = new Set(value.uiStates.map((state) => state.name));
|
|
203
|
+
for (const target of value.verificationTargets) {
|
|
204
|
+
// Verification evidence may point to read-only project files such as
|
|
205
|
+
// tsconfig.json or a test entrypoint; implementation targets remain
|
|
206
|
+
// governed by targets.files and the writer writeSet.
|
|
207
|
+
for (const requirementId of target.requirementIds) {
|
|
208
|
+
if (!known.has(requirementId)) {
|
|
209
|
+
ctx.addIssue({
|
|
210
|
+
code: "custom",
|
|
211
|
+
message: `verification target references unknown requirement ${requirementId}`,
|
|
212
|
+
path: ["verificationTargets"],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const stateName of target.uiStates) {
|
|
217
|
+
if (!stateNames.has(stateName)) {
|
|
218
|
+
ctx.addIssue({
|
|
219
|
+
code: "custom",
|
|
220
|
+
message: `verification target references unknown UI state ${stateName}`,
|
|
221
|
+
path: ["verificationTargets"],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
201
226
|
for (const requirement of value.requirements) {
|
|
202
227
|
if (!known.has(requirement.id))
|
|
203
228
|
ctx.addIssue({
|
|
@@ -238,7 +263,70 @@ export const frontendImplementationContractSchema = z
|
|
|
238
263
|
path: ["uiStates"],
|
|
239
264
|
});
|
|
240
265
|
}
|
|
266
|
+
if (value.mockApi.strategy !== "not-needed") {
|
|
267
|
+
if (value.mockApi.endpoints.length === 0) {
|
|
268
|
+
ctx.addIssue({
|
|
269
|
+
code: "custom",
|
|
270
|
+
message: "Mock strategy requires at least one endpoint",
|
|
271
|
+
path: ["mockApi", "endpoints"],
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
value.mockApi.endpoints.forEach((endpoint, index) => {
|
|
275
|
+
if (!endpoint.fixture) {
|
|
276
|
+
ctx.addIssue({
|
|
277
|
+
code: "custom",
|
|
278
|
+
message: "Mock endpoint requires a fixture path",
|
|
279
|
+
path: ["mockApi", "endpoints", index, "fixture"],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (!endpoint.consumer) {
|
|
283
|
+
ctx.addIssue({
|
|
284
|
+
code: "custom",
|
|
285
|
+
message: "Mock endpoint requires a consumer path",
|
|
286
|
+
path: ["mockApi", "endpoints", index, "consumer"],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
for (const [field, file] of [["fixture", endpoint.fixture], ["consumer", endpoint.consumer]]) {
|
|
290
|
+
if (file && !value.targets.files.some((pattern) => pathMatchesPattern(file, pattern))) {
|
|
291
|
+
ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
message: `Mock ${field} is outside contract targets: ${file}`,
|
|
294
|
+
path: ["mockApi", "endpoints", index, field],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
241
300
|
});
|
|
301
|
+
export async function assertFrontendSourceBindingFresh(input) {
|
|
302
|
+
for (const source of input.binding.sources) {
|
|
303
|
+
// DAG source bindings are task-relative (for example source/需求.md),
|
|
304
|
+
// while older bindings may already contain the repository-relative
|
|
305
|
+
// .harness/tasks/<taskId>/ prefix. Resolve both forms before hashing.
|
|
306
|
+
const taskDir = path.resolve(input.workspaceRoot, ".harness", "tasks", input.binding.taskId);
|
|
307
|
+
const absolute = source.path.startsWith(".harness/")
|
|
308
|
+
? path.resolve(input.workspaceRoot, source.path)
|
|
309
|
+
: path.resolve(taskDir, source.path);
|
|
310
|
+
const relative = path.relative(input.workspaceRoot, absolute);
|
|
311
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
312
|
+
throw new Error(`frontend source binding escapes workspace: ${source.path}`);
|
|
313
|
+
}
|
|
314
|
+
let content;
|
|
315
|
+
try {
|
|
316
|
+
content = await readFile(absolute);
|
|
317
|
+
const info = await stat(absolute);
|
|
318
|
+
if (!info.isFile())
|
|
319
|
+
throw new Error("not a regular file");
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
throw new Error(`frontend source binding file unavailable: ${source.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
323
|
+
}
|
|
324
|
+
const actual = createHash("sha256").update(content).digest("hex");
|
|
325
|
+
if (actual !== source.sha256) {
|
|
326
|
+
throw new Error(`frontend source binding is stale: ${source.path}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
242
330
|
const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
|
|
243
331
|
const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
|
|
244
332
|
function secretIssues(value, at = "$", issues = []) {
|
|
@@ -314,9 +402,42 @@ function looksLikeStrictFrontendContract(value) {
|
|
|
314
402
|
* Near-schema payloads are left untouched so unknown-key fail-closed still holds.
|
|
315
403
|
*/
|
|
316
404
|
export function coerceFrontendImplementationContractInput(value, canonicalBinding) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
405
|
+
const rawRecord = asRecord(value);
|
|
406
|
+
const verificationTargetIds = rawRecord && Array.isArray(rawRecord.verificationTargets)
|
|
407
|
+
? new Set(rawRecord.verificationTargets
|
|
408
|
+
.map((item) => asString(asRecord(item)?.id))
|
|
409
|
+
.filter(Boolean))
|
|
410
|
+
: undefined;
|
|
411
|
+
const normalizedValue = rawRecord
|
|
412
|
+
? {
|
|
413
|
+
...rawRecord,
|
|
414
|
+
requirements: verificationTargetIds && Array.isArray(rawRecord.requirements)
|
|
415
|
+
? rawRecord.requirements.map((item) => {
|
|
416
|
+
const requirement = asRecord(item);
|
|
417
|
+
if (!requirement || !Array.isArray(requirement.verificationTargetIds))
|
|
418
|
+
return item;
|
|
419
|
+
return {
|
|
420
|
+
...requirement,
|
|
421
|
+
verificationTargetIds: requirement.verificationTargetIds.filter((id) => typeof id === "string" && verificationTargetIds.has(id)),
|
|
422
|
+
};
|
|
423
|
+
})
|
|
424
|
+
: rawRecord.requirements,
|
|
425
|
+
uiStates: Array.isArray(rawRecord.uiStates)
|
|
426
|
+
? rawRecord.uiStates.map((item) => {
|
|
427
|
+
const state = asRecord(item);
|
|
428
|
+
if (!state ||
|
|
429
|
+
state.applicable === false ||
|
|
430
|
+
(state.notApplicableReason !== "" && state.notApplicableReason !== null))
|
|
431
|
+
return item;
|
|
432
|
+
const { notApplicableReason: _emptyReason, ...withoutEmptyReason } = state;
|
|
433
|
+
return withoutEmptyReason;
|
|
434
|
+
})
|
|
435
|
+
: rawRecord.uiStates,
|
|
436
|
+
}
|
|
437
|
+
: value;
|
|
438
|
+
if (looksLikeStrictFrontendContract(normalizedValue))
|
|
439
|
+
return normalizedValue;
|
|
440
|
+
const record = asRecord(normalizedValue);
|
|
320
441
|
if (!record)
|
|
321
442
|
return value;
|
|
322
443
|
const implementation = asRecord(record.implementation);
|
|
@@ -333,7 +454,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
333
454
|
...asStringArray(asRecord(component?.paths)?.domHelper ? [asRecord(component?.paths)?.domHelper] : []),
|
|
334
455
|
].filter((item, index, arr) => arr.indexOf(item) === index);
|
|
335
456
|
if (targetFiles.length === 0) {
|
|
336
|
-
|
|
457
|
+
throw new Error("frontend contract compatibility input must declare target files");
|
|
337
458
|
}
|
|
338
459
|
const riskRaw = asString(record.riskLevel) ||
|
|
339
460
|
asString(record.risk) ||
|
|
@@ -383,21 +504,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
383
504
|
});
|
|
384
505
|
}
|
|
385
506
|
if (verificationTargets.length === 0) {
|
|
386
|
-
|
|
387
|
-
id: "VT-STATIC",
|
|
388
|
-
type: "static",
|
|
389
|
-
commandLabel: "npm run typecheck",
|
|
390
|
-
file: targetFiles[0],
|
|
391
|
-
requirementIds: [...canonicalBinding.requirementIds],
|
|
392
|
-
uiStates: ["success"],
|
|
393
|
-
}, {
|
|
394
|
-
id: "VT-UNIT",
|
|
395
|
-
type: "unit",
|
|
396
|
-
commandLabel: "npm run test:unit:fe",
|
|
397
|
-
file: targetFiles.find((p) => p.includes("__tests__")) || targetFiles[0],
|
|
398
|
-
requirementIds: [...canonicalBinding.requirementIds],
|
|
399
|
-
uiStates: ["success", "error"],
|
|
400
|
-
});
|
|
507
|
+
throw new Error("frontend contract compatibility input must declare verification targets");
|
|
401
508
|
}
|
|
402
509
|
const defaultVerificationIds = verificationTargets.map((item) => String(item.id));
|
|
403
510
|
const requirements = [];
|
|
@@ -440,11 +547,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
440
547
|
}
|
|
441
548
|
for (const id of canonicalBinding.requirementIds) {
|
|
442
549
|
if (!requirements.some((item) => item.id === id)) {
|
|
443
|
-
|
|
444
|
-
id,
|
|
445
|
-
implementationTargets: targetFiles,
|
|
446
|
-
verificationTargetIds: defaultVerificationIds,
|
|
447
|
-
});
|
|
550
|
+
throw new Error(`frontend contract compatibility input does not cover ${id}`);
|
|
448
551
|
}
|
|
449
552
|
}
|
|
450
553
|
const uiStates = [];
|
|
@@ -511,14 +614,20 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
511
614
|
});
|
|
512
615
|
}
|
|
513
616
|
const strategyRaw = asString(mockApiIn.strategy) || "not-needed";
|
|
514
|
-
const
|
|
617
|
+
const allowedStrategies = [
|
|
515
618
|
"native",
|
|
516
619
|
"browser-intercept",
|
|
517
620
|
"request-adapter",
|
|
518
621
|
"not-needed",
|
|
519
|
-
]
|
|
520
|
-
|
|
521
|
-
:
|
|
622
|
+
];
|
|
623
|
+
if (!allowedStrategies.includes(strategyRaw)) {
|
|
624
|
+
throw new Error(`frontend contract compatibility input has unsupported Mock strategy: ${strategyRaw}`);
|
|
625
|
+
}
|
|
626
|
+
const strategy = strategyRaw;
|
|
627
|
+
const productionDefaultOff = mockApiIn.productionDefaultOff === true || mockApiIn.productionMockOff === true;
|
|
628
|
+
if (!productionDefaultOff) {
|
|
629
|
+
throw new Error("frontend contract compatibility input must prove production Mock is off");
|
|
630
|
+
}
|
|
522
631
|
const activation = asString(mockApiIn.activation) ||
|
|
523
632
|
(strategy === "not-needed"
|
|
524
633
|
? "production remains real fetch; unit tests may inject fetchImpl only"
|