chati-dev 4.3.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -13
- package/bin/chati.js +32 -4
- package/framework/agents/build/dev.md +9 -5
- package/framework/agents/discover/brief.md +2 -0
- package/framework/agents/discover/brownfield-wu.md +2 -0
- package/framework/agents/discover/greenfield-wu.md +18 -1
- package/framework/agents/plan/detail.md +2 -0
- package/framework/agents/plan/tasks.md +31 -0
- package/framework/agents/plan/ux-brand-architect.md +21 -2
- package/framework/agents/plan/ux-component-engineer.md +10 -0
- package/framework/agents/quality/qa-implementation.md +1 -1
- package/framework/agents/quality/qa-planning.md +1 -1
- package/framework/agents/quality/qa-visual.md +33 -21
- package/framework/config.yaml +5 -4
- package/framework/constitution.md +58 -11
- package/framework/context/governance.md +12 -10
- package/framework/context/root.md +2 -2
- package/framework/data/entity-registry.yaml +12 -4
- package/framework/data/qa-rubrics.yaml +335 -0
- package/framework/domains/constitution.yaml +3 -3
- package/framework/executors/json-validate.js +80 -0
- package/framework/executors/npm-script.js +72 -0
- package/framework/executors/path-check.js +67 -0
- package/framework/executors/registry.yaml +36 -0
- package/framework/hooks/advance-trigger.js +47 -3
- package/framework/hooks/constitution-guard.js +9 -4
- package/framework/hooks/git-push-authority.js +113 -0
- package/framework/hooks/license-guard.js +59 -9
- package/framework/hooks/model-governance.js +40 -17
- package/framework/hooks/prism-engine.js +78 -11
- package/framework/hooks/session-digest.js +12 -3
- package/framework/hooks/settings.json +7 -3
- package/framework/hooks/style-guard.js +52 -3
- package/framework/hooks/team-quality-gate.js +43 -5
- package/framework/i18n/en.yaml +3 -3
- package/framework/i18n/es.yaml +3 -3
- package/framework/i18n/fr.yaml +3 -3
- package/framework/i18n/pt.yaml +3 -3
- package/framework/intelligence/confidence.yaml +85 -6
- package/framework/intelligence/context-engine.md +9 -5
- package/framework/intelligence/decision-engine.md +1 -1
- package/framework/orchestrator/chati-router.js +59 -5
- package/framework/orchestrator/chati-update.md +19 -3
- package/framework/orchestrator/chati.md +20 -1
- package/framework/quality-gates/planning-gate.md +2 -0
- package/framework/rules/human-writing-style.md +47 -0
- package/framework/scaffold/motion-premium/README.md +12 -2
- package/framework/scaffold/motion-premium/lib/animations/tokens.ts.template +1 -1
- package/framework/scaffold/motion-premium/scaffold.yaml +6 -1
- package/framework/scaffold/motion-premium-3d/README.md +10 -0
- package/framework/scaffold/motion-premium-3d/components/webgl/WebGLContext.tsx.template +1 -1
- package/framework/scaffold/motion-premium-3d/scaffold.yaml +3 -0
- package/framework/scaffold/saas-dashboard/README.md +58 -0
- package/framework/scaffold/saas-dashboard/app/dashboard/page.tsx.template +546 -0
- package/framework/scaffold/saas-dashboard/app/globals.css.template +746 -0
- package/framework/scaffold/saas-dashboard/app/layout.tsx.template +192 -0
- package/framework/scaffold/saas-dashboard/app/shell.css.template +1070 -0
- package/framework/scaffold/saas-dashboard/components/data/Chart.tsx.template +725 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.tsx.template +623 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTable.types.ts.template +104 -0
- package/framework/scaffold/saas-dashboard/components/data/DataTablePagination.tsx.template +234 -0
- package/framework/scaffold/saas-dashboard/components/data/SkeletonBlock.tsx.template +111 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCard.tsx.template +393 -0
- package/framework/scaffold/saas-dashboard/components/data/StatCardGrid.tsx.template +121 -0
- package/framework/scaffold/saas-dashboard/components/feedback/EmptyState.tsx.template +281 -0
- package/framework/scaffold/saas-dashboard/components/interaction/CommandPalette.tsx.template +767 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Field.tsx.template +351 -0
- package/framework/scaffold/saas-dashboard/components/interaction/Form.tsx.template +424 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Badge.tsx.template +193 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Button.tsx.template +261 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Card.tsx.template +287 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Input.tsx.template +310 -0
- package/framework/scaffold/saas-dashboard/components/primitives/Spinner.tsx.template +135 -0
- package/framework/scaffold/saas-dashboard/components/primitives/index.ts.template +60 -0
- package/framework/scaffold/saas-dashboard/components/shell/AppShell.tsx.template +174 -0
- package/framework/scaffold/saas-dashboard/components/shell/Breadcrumb.tsx.template +186 -0
- package/framework/scaffold/saas-dashboard/components/shell/Container.tsx.template +86 -0
- package/framework/scaffold/saas-dashboard/components/shell/NavItem.tsx.template +147 -0
- package/framework/scaffold/saas-dashboard/components/shell/Sidebar.tsx.template +378 -0
- package/framework/scaffold/saas-dashboard/components/shell/SidebarContext.tsx.template +169 -0
- package/framework/scaffold/saas-dashboard/components/shell/ThemeToggle.tsx.template +128 -0
- package/framework/scaffold/saas-dashboard/components/shell/Topbar.tsx.template +301 -0
- package/framework/scaffold/saas-dashboard/lib/brand.ts.template +324 -0
- package/framework/scaffold/saas-dashboard/scaffold.yaml +225 -0
- package/framework/schemas/session.schema.json +5 -0
- package/framework/schemas/task.schema.json +9 -0
- package/framework/scripts/visual-qa.js +101 -3
- package/framework/tasks/orchestrator-health.md +4 -4
- package/framework/tasks/qa-impl-verdict.md +11 -5
- package/framework/templates/brandbook-html-tmpl.md +1 -1
- package/framework/templates/qa-gate-tmpl.yaml +24 -11
- package/package.json +4 -3
- package/src/config/context-file-generator.js +0 -6
- package/src/dashboard/renderer.js +0 -36
- package/src/executors/runner.js +204 -0
- package/src/installer/core.js +13 -3
- package/src/installer/templates.js +5 -6
- package/src/intelligence/registry-manager.js +1 -1
- package/src/license/client.js +9 -3
- package/src/license/commands.js +12 -2
- package/src/license/machine-id.js +42 -1
- package/src/memory/gotchas.js +58 -40
- package/src/memory/magic-docs.js +1 -1
- package/src/memory/session-digest.js +9 -4
- package/src/orchestrator/cli.js +186 -15
- package/src/orchestrator/doctor.js +98 -16
- package/src/orchestrator/pipeline-manager.js +44 -22
- package/src/orchestrator/session-manager.js +64 -8
- package/src/telemetry/sender.js +7 -19
- package/src/terminal/run-team.js +3 -3
- package/src/upgrade/tracked-files-detector.js +34 -16
- package/src/utils/feature-flags.js +1 -1
- package/src/utils/flatten-entities.js +4 -40
- package/src/utils/schema-validator.js +0 -14
- package/src/wizard/i18n.js +3 -3
- package/src/api/index.js +0 -120
- package/src/autonomy/autonomous-gate.js +0 -294
- package/src/autonomy/build-loop.js +0 -281
- package/src/autonomy/build-state.js +0 -286
- package/src/autonomy/cause-analyzer.js +0 -177
- package/src/autonomy/escalation.js +0 -214
- package/src/autonomy/index.js +0 -51
- package/src/autonomy/mode-manager.js +0 -225
- package/src/autonomy/mode-suggester.js +0 -283
- package/src/autonomy/progress-reporter.js +0 -275
- package/src/autonomy/safety-net.js +0 -370
- package/src/config/agent-customizer.js +0 -231
- package/src/decision/analyzer.js +0 -291
- package/src/decision/engine.js +0 -250
- package/src/decision/index.js +0 -38
- package/src/decision/registry-healer.js +0 -468
- package/src/decision/registry-updater.js +0 -339
- package/src/extensions/loader.js +0 -145
- package/src/extensions/registry.js +0 -134
- package/src/gates/circuit-breaker.js +0 -151
- package/src/gates/g1-planning-complete.js +0 -154
- package/src/gates/g2-qa-planning.js +0 -156
- package/src/gates/g3-implementation.js +0 -215
- package/src/gates/g4-qa-implementation.js +0 -240
- package/src/gates/g5-deploy-ready.js +0 -181
- package/src/gates/gate-base.js +0 -185
- package/src/gates/index.js +0 -46
- package/src/health/auto-fix.js +0 -216
- package/src/health/engine.js +0 -246
- package/src/merger/semantic-merger.js +0 -292
- package/src/preview/detector.js +0 -238
- package/src/preview/index.js +0 -20
- package/src/preview/launcher.js +0 -235
- package/src/preview/log-buffer.js +0 -103
- package/src/quality/metrics-collector.js +0 -281
- package/src/quality/test-runner.js +0 -366
- package/src/tasks/executor.js +0 -195
- package/src/tasks/index.js +0 -4
- package/src/tasks/loader.js +0 -210
- package/src/tasks/router.js +0 -182
- package/src/utils/event-bus.js +0 -126
package/src/orchestrator/cli.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { resolveFrameworkDir } from '../utils/framework-dir.js';
|
|
15
|
+
import { isFeatureEnabled } from '../utils/feature-flags.js';
|
|
16
|
+
import { runScriptTask, formatScriptHandoff } from '../executors/runner.js';
|
|
15
17
|
|
|
16
18
|
import {
|
|
17
19
|
loadSession, initSession, updateSession, recordAgentCompletion,
|
|
@@ -24,6 +26,7 @@ import {
|
|
|
24
26
|
import { resolveContextLimit } from '../utils/provider-limits.js';
|
|
25
27
|
import { waitForLicense } from '../license/wait.js';
|
|
26
28
|
import { runDoctor } from './doctor.js';
|
|
29
|
+
import { evaluateSurfaceCriteria } from '../autonomy/surface-criteria.js';
|
|
27
30
|
import {
|
|
28
31
|
getNextAgent, getAgentDefinition, AGENT_PIPELINE,
|
|
29
32
|
} from './index.js';
|
|
@@ -38,7 +41,7 @@ import {
|
|
|
38
41
|
import { analyzeDeviationImpact, applyDeviation } from './index.js';
|
|
39
42
|
import { detectQuickFlow, detectStandardFlow } from './index.js';
|
|
40
43
|
import { getAgentFile } from '../terminal/prompt-builder.js';
|
|
41
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync
|
|
44
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
42
45
|
import { join, resolve } from 'path';
|
|
43
46
|
import { execSync } from 'child_process';
|
|
44
47
|
import yaml from 'js-yaml';
|
|
@@ -634,6 +637,17 @@ function writeSessionLock(projectDir, currentAgent, stateInfo = {}) {
|
|
|
634
637
|
const localMdPath = join(projectDir, 'CLAUDE.local.md');
|
|
635
638
|
let content = existsSync(localMdPath) ? readFileSync(localMdPath, 'utf-8') : '';
|
|
636
639
|
|
|
640
|
+
// Phase display is authoritatively driven by session.mode on disk. Callers
|
|
641
|
+
// previously passed stateInfo.phase derived from transient intermediate
|
|
642
|
+
// values (the next agent's expected phase before user approval), causing
|
|
643
|
+
// the lock to drift ahead of session.yaml (e.g. show "build" while
|
|
644
|
+
// session.mode was still "plan" awaiting approval).
|
|
645
|
+
let authoritativePhase = null;
|
|
646
|
+
try {
|
|
647
|
+
const { loaded, session } = loadSession(projectDir);
|
|
648
|
+
if (loaded && session?.mode) authoritativePhase = session.mode;
|
|
649
|
+
} catch { /* non-fatal — fall back to stateInfo */ }
|
|
650
|
+
|
|
637
651
|
const lockInner = `## Session Lock -- ACTIVE
|
|
638
652
|
|
|
639
653
|
**Chati.dev session is ACTIVE.** Follow these rules for EVERY message:
|
|
@@ -645,7 +659,7 @@ function writeSessionLock(projectDir, currentAgent, stateInfo = {}) {
|
|
|
645
659
|
|
|
646
660
|
const stateInner = `## Current State
|
|
647
661
|
- **Agent**: ${currentAgent || 'None'}
|
|
648
|
-
- **Phase**: ${stateInfo.phase || 'discover'}
|
|
662
|
+
- **Phase**: ${authoritativePhase || stateInfo.phase || 'discover'}
|
|
649
663
|
- **Pipeline**: ${stateInfo.position ?? 0}/${stateInfo.total ?? '?'} (${stateInfo.progress ?? 0}%)
|
|
650
664
|
- **Mode**: ${stateInfo.mode || 'interactive'}`;
|
|
651
665
|
|
|
@@ -920,6 +934,25 @@ async function _handleNextInner(projectDir) {
|
|
|
920
934
|
spawnCommand = buildSpawnCommand(nextAgent, projectDir, lastAgent, modelInfo.provider, 600000);
|
|
921
935
|
}
|
|
922
936
|
|
|
937
|
+
// Surface criteria (Article XVII): even in autonomous mode, a high-stakes
|
|
938
|
+
// transition still needs a human nod. Deploy is external and effectively
|
|
939
|
+
// irreversible, so it trips a critical criterion and is never auto-confirmed
|
|
940
|
+
// by the gates alone. Hold the autonomous spawn and hand control back to the
|
|
941
|
+
// user for that step.
|
|
942
|
+
let surfaceHold = null;
|
|
943
|
+
if (action === 'spawn_autonomous' && (agentDef?.phase === 'deploy' || nextAgent === 'devops')) {
|
|
944
|
+
const verdict = evaluateSurfaceCriteria(
|
|
945
|
+
{ type: 'deploy', description: `${nextAgent} (${agentDef?.phase || 'deploy'} phase)`, tags: ['external', 'destructive'] },
|
|
946
|
+
{},
|
|
947
|
+
);
|
|
948
|
+
const critical = verdict.triggers.filter((t) => t.severity === 'critical');
|
|
949
|
+
if (critical.length > 0) {
|
|
950
|
+
action = 'activate_interactive';
|
|
951
|
+
spawnCommand = null;
|
|
952
|
+
surfaceHold = { criteria: critical.map((t) => t.id), reason: critical.map((t) => t.reason).join('; ') };
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
923
956
|
// Record agent activation event
|
|
924
957
|
recordEvent(projectDir, EventType.AGENT_ACTIVATED, nextAgent, { action, phase: agentDef?.phase });
|
|
925
958
|
|
|
@@ -941,6 +974,7 @@ async function _handleNextInner(projectDir) {
|
|
|
941
974
|
pipeline_progress: progress,
|
|
942
975
|
session: { language: session.language, project_type: session.project_type || session.project?.type, execution_mode: session.execution_mode, user_level: session.user_level || 'auto' },
|
|
943
976
|
status_summary: `${lastAgent} completed. Next: ${nextAgent} (${action.replace('_', ' ')}, ${agentDef?.phase || 'unknown'} phase).`,
|
|
977
|
+
...(surfaceHold ? { surface: surfaceHold } : {}),
|
|
944
978
|
};
|
|
945
979
|
|
|
946
980
|
// Include team data when spawn_team action is selected (Article XXI)
|
|
@@ -978,6 +1012,7 @@ const SCAFFOLD_CONFIDENCE_THRESHOLD = 0.7;
|
|
|
978
1012
|
const PRESET_CONFIDENCE_THRESHOLDS = {
|
|
979
1013
|
'motion-premium': 0.7,
|
|
980
1014
|
'motion-premium-3d': 0.8,
|
|
1015
|
+
'saas-dashboard': 0.7,
|
|
981
1016
|
};
|
|
982
1017
|
|
|
983
1018
|
function thresholdFor(preset) {
|
|
@@ -1526,6 +1561,28 @@ async function handleAdvance(projectDir, args) {
|
|
|
1526
1561
|
},
|
|
1527
1562
|
};
|
|
1528
1563
|
}
|
|
1564
|
+
|
|
1565
|
+
// Spring-motion veto: linear easing on a transition and transition-property:
|
|
1566
|
+
// all are amateur-motion tells measured by visual-qa.js. Block the
|
|
1567
|
+
// autonomous advance on them, the same way layout_errors block, so the code
|
|
1568
|
+
// gate enforces premium motion and not only the QA-Visual agent prose.
|
|
1569
|
+
const motionErrors = (report.summary?.motion_errors || []).filter(e => e.severity === 'error');
|
|
1570
|
+
if (motionErrors.length > 0) {
|
|
1571
|
+
return {
|
|
1572
|
+
advanced: false,
|
|
1573
|
+
blocked: true,
|
|
1574
|
+
reason: 'motion_errors_detected',
|
|
1575
|
+
message: `${motionErrors.length} amateur-motion error(s) measured: linear easing on a transition, or transition-property: all. Premium motion uses spring or cubic-bezier easing on specific compositor properties (transform, opacity).`,
|
|
1576
|
+
motion_error_count: motionErrors.length,
|
|
1577
|
+
motion_errors: motionErrors,
|
|
1578
|
+
fault_origin: 'CODE',
|
|
1579
|
+
action_required: {
|
|
1580
|
+
step1: 'Display the motion errors to the user with the offending selectors',
|
|
1581
|
+
step2: 'Route back to dev for rework (Fault Vector Protocol: CODE -> dev)',
|
|
1582
|
+
step3: 'Dev replaces linear/all easing with a cubic-bezier on transform/opacity, re-runs visual-qa.js, then re-runs advance qa-visual',
|
|
1583
|
+
},
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1529
1586
|
} catch {
|
|
1530
1587
|
// report.json unparseable — fall through to other checks, they will catch it
|
|
1531
1588
|
}
|
|
@@ -2930,7 +2987,7 @@ async function handleWaitForCapture(projectDir, args) {
|
|
|
2930
2987
|
* qa-visual-score: calculate visual QA score deterministically from report.json.
|
|
2931
2988
|
* Replaces QA-Visual's generative scoring formula.
|
|
2932
2989
|
*/
|
|
2933
|
-
function handleQaVisualScore(args) {
|
|
2990
|
+
export function handleQaVisualScore(args) {
|
|
2934
2991
|
const reportPath = args.report || '/tmp/visual-qa/report.json';
|
|
2935
2992
|
if (!existsSync(reportPath)) {
|
|
2936
2993
|
return errorResult(`report.json not found at ${reportPath}`, 'REPORT_NOT_FOUND');
|
|
@@ -2944,18 +3001,51 @@ function handleQaVisualScore(args) {
|
|
|
2944
3001
|
const s = report.summary || {};
|
|
2945
3002
|
const hasRefs = args['has-refs'] === 'true';
|
|
2946
3003
|
|
|
3004
|
+
// The animation stack the project actually uses. The motion-premium presets
|
|
3005
|
+
// ship GSAP + Lenis; the saas-dashboard app-shell preset ships framer-motion;
|
|
3006
|
+
// some projects use CSS only. The Lenis/GSAP presence checks apply ONLY to the
|
|
3007
|
+
// gsap-lenis stack, so a framer-motion project is not falsely blocked for
|
|
3008
|
+
// missing libraries it deliberately does not use. The agent resolves the stack
|
|
3009
|
+
// from architecture.md and passes --animation-library (default gsap-lenis for
|
|
3010
|
+
// backward compatibility with the motion presets).
|
|
3011
|
+
// Default to gsap-lenis for backward compatibility (the motion presets), but do
|
|
3012
|
+
// NOT assume it when the flag is absent AND the report shows no GSAP or Lenis
|
|
3013
|
+
// usage at all: a clean framer-motion or CSS project would otherwise be
|
|
3014
|
+
// phantom-blocked for libraries it never used. In that case fall back to a
|
|
3015
|
+
// non-GSAP stack (no GSAP/Lenis hard-block) and warn that --animation-library
|
|
3016
|
+
// should be passed explicitly. A project that passes the flag is unaffected.
|
|
3017
|
+
const animLibFlag = args['animation-library'];
|
|
3018
|
+
const animLibInferred = !animLibFlag;
|
|
3019
|
+
let animLib = String(animLibFlag || 'gsap-lenis').toLowerCase();
|
|
3020
|
+
if (animLibInferred && !s.gsap_loaded_all && !(s.total_gsap_animations > 0) && !s.lenis_active_all) {
|
|
3021
|
+
animLib = 'unknown';
|
|
3022
|
+
}
|
|
3023
|
+
const isGsapStack = animLib === 'gsap-lenis' || animLib === 'gsap' || animLib === 'lenis';
|
|
3024
|
+
|
|
3025
|
+
// Measured error signals from the report (objective, not judged).
|
|
3026
|
+
const motionErrorCount = (s.motion_errors || []).filter(e => e.severity === 'error').length;
|
|
3027
|
+
const layoutErrorCount = s.layout_error_count || 0;
|
|
3028
|
+
const reducedMotionViolations = (s.reduced_motion_violations || []).length;
|
|
3029
|
+
|
|
2947
3030
|
// Weights (adjust when references exist)
|
|
2948
3031
|
const w = hasRefs
|
|
2949
3032
|
? { animation: 0.30, scroll: 0.20, responsive: 0.15, hover: 0.10, brand: 0.10, fidelity: 0.15 }
|
|
2950
3033
|
: { animation: 0.35, scroll: 0.25, responsive: 0.20, hover: 0.10, brand: 0.10, fidelity: 0 };
|
|
2951
3034
|
|
|
2952
|
-
//
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3035
|
+
// Animation score is stack-aware. The gsap-lenis stack scores on Lenis + GSAP
|
|
3036
|
+
// presence and animation count; any other stack scores on the motion discipline
|
|
3037
|
+
// the report can measure: reduced-motion support and the absence of amateur
|
|
3038
|
+
// motion (the spring-motion veto), since premium motion can be built without
|
|
3039
|
+
// GSAP.
|
|
3040
|
+
const animScore = isGsapStack
|
|
3041
|
+
? Math.min(100, (s.lenis_active_all ? 50 : 0) + (s.gsap_loaded_all ? 30 : 0) + Math.min(20, (s.total_gsap_animations || 0) * 2))
|
|
3042
|
+
: Math.max(0, Math.min(100, 70 + (s.reduced_motion_support ? 20 : 0) + (motionErrorCount === 0 ? 10 : -30)));
|
|
3043
|
+
|
|
3044
|
+
const scrollScore = (s.total_scroll_triggers || 0) > 0 ? 80 + Math.min(20, s.total_scroll_triggers * 2) : (isGsapStack ? 40 : 80);
|
|
3045
|
+
const responsiveScore = layoutErrorCount > 0 ? 40 : 80; // layout errors are measured; the agent judges the rest
|
|
3046
|
+
const hoverScore = 80; // baseline; the agent judges interaction states from screenshots
|
|
2957
3047
|
const brandScore = (s.reduced_motion_support ? 10 : 0) + Math.min(90, (report.pages?.[0]?.js_checks?.custom_properties_count ?? 0));
|
|
2958
|
-
const fidelityScore = 70; // placeholder
|
|
3048
|
+
const fidelityScore = 70; // placeholder; the Visualizer comparison is the agent's judged signal
|
|
2959
3049
|
|
|
2960
3050
|
let rawScore = Math.round(
|
|
2961
3051
|
animScore * w.animation +
|
|
@@ -2970,18 +3060,96 @@ function handleQaVisualScore(args) {
|
|
|
2970
3060
|
const errors = [];
|
|
2971
3061
|
const warnings = [];
|
|
2972
3062
|
|
|
2973
|
-
if (!s.lenis_active_all) errors.push('Lenis not active on all pages');
|
|
2974
|
-
if (!s.gsap_loaded_all) errors.push('GSAP not loaded on all pages');
|
|
3063
|
+
if (isGsapStack && !s.lenis_active_all) errors.push('Lenis not active on all pages');
|
|
3064
|
+
if (isGsapStack && !s.gsap_loaded_all) errors.push('GSAP not loaded on all pages');
|
|
3065
|
+
if (motionErrorCount > 0) { errors.push(`${motionErrorCount} amateur-motion error(s): linear easing or transition:all`); rawScore -= 15; }
|
|
3066
|
+
if (layoutErrorCount > 0) { errors.push(`${layoutErrorCount} objective layout error(s)`); rawScore -= 15; }
|
|
3067
|
+
if (reducedMotionViolations > 0) { errors.push(`${reducedMotionViolations} reduced-motion violation(s)`); rawScore -= 10; }
|
|
2975
3068
|
if (s.em_dashes_detected) { errors.push('Em-dashes found in visible text'); rawScore -= 10; }
|
|
2976
3069
|
if (s.console_errors_total > 0) { warnings.push(`${s.console_errors_total} console error(s)`); rawScore -= 5; }
|
|
3070
|
+
if (animLibInferred) warnings.push('No --animation-library was passed; resolve the motion stack from architecture.md and pass it (gsap-lenis | framer-motion | css | none) for an accurate verdict.');
|
|
3071
|
+
|
|
3072
|
+
// Hard blocks: measured failures that cannot be APPROVED regardless of the
|
|
3073
|
+
// agent's judged 8-dimension rubric score. The agent applies visual_rubric on
|
|
3074
|
+
// top of this measured floor and SHALL NOT issue APPROVED when hardBlocked.
|
|
3075
|
+
const hardBlocked =
|
|
3076
|
+
(isGsapStack && (!s.lenis_active_all || !s.gsap_loaded_all)) ||
|
|
3077
|
+
motionErrorCount > 0 ||
|
|
3078
|
+
layoutErrorCount > 0 ||
|
|
3079
|
+
reducedMotionViolations > 0 ||
|
|
3080
|
+
!!s.em_dashes_detected;
|
|
2977
3081
|
|
|
2978
|
-
// Hard blocks
|
|
2979
|
-
const hardBlocked = !s.lenis_active_all || !s.gsap_loaded_all || s.em_dashes_detected;
|
|
2980
3082
|
const score = Math.max(0, Math.min(100, rawScore));
|
|
2981
3083
|
const threshold = 90;
|
|
2982
|
-
|
|
3084
|
+
// FLOOR_COMPUTED (not NEEDS_CORRECTION) is the label for a clean-but-below-floor
|
|
3085
|
+
// measured score: it signals that this is the CLI's measured floor, NOT the gate
|
|
3086
|
+
// verdict. The gate verdict is the QA-Visual agent's 8-dimension rubric score. A
|
|
3087
|
+
// clean framer-motion build can sit below the measured floor yet still be APPROVED
|
|
3088
|
+
// by the agent, so the CLI must not emit a correction-sounding label here.
|
|
3089
|
+
const verdict = hardBlocked ? 'BLOCKED' : score >= threshold ? 'APPROVED' : 'FLOOR_COMPUTED';
|
|
2983
3090
|
|
|
2984
|
-
return {
|
|
3091
|
+
return {
|
|
3092
|
+
score, threshold, verdict, hardBlocked, animation_library: animLib, errors, warnings, weights: w,
|
|
3093
|
+
measured_signals: {
|
|
3094
|
+
motion_error_count: motionErrorCount,
|
|
3095
|
+
layout_error_count: layoutErrorCount,
|
|
3096
|
+
reduced_motion_violations: reducedMotionViolations,
|
|
3097
|
+
em_dashes: !!s.em_dashes_detected,
|
|
3098
|
+
},
|
|
3099
|
+
dimensions: { animScore, scrollScore, responsiveScore, hoverScore, brandScore, fidelityScore },
|
|
3100
|
+
note: 'Measured objective floor plus hard-blocks. BLOCKED is binding (a measured failure). APPROVED and FLOOR_COMPUTED are NOT the gate verdict: the QA-Visual agent applies the 8-dimension visual_rubric (chati.dev/data/qa-rubrics.yaml) for the judged dimensions and issues the verdict. FLOOR_COMPUTED means the measured floor is below the threshold but nothing is hard-blocked, so the agent rubric score decides. The agent cannot issue APPROVED when hardBlocked is true.',
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
/**
|
|
3105
|
+
* run-script-task: execute a task whose execution_kind is `script` through its
|
|
3106
|
+
* deterministic executor (no LLM). Gated by the execution_kinds feature flag;
|
|
3107
|
+
* when the flag is off it returns ran:false so the orchestrator uses the agent
|
|
3108
|
+
* path and the pipeline is unchanged. The orchestrator resolves the task fields
|
|
3109
|
+
* from tasks.yaml and passes them as flags (--ref, --id, --title).
|
|
3110
|
+
*/
|
|
3111
|
+
export async function handleRunScriptTask(projectDir, args) {
|
|
3112
|
+
if (!isFeatureEnabled(projectDir, 'execution_kinds')) {
|
|
3113
|
+
return { ran: false, reason: 'The execution_kinds feature is off. Run this task through the agent path.', fallback: 'agent' };
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const kind = String(args.kind || 'script');
|
|
3117
|
+
if (kind !== 'script') {
|
|
3118
|
+
return { ran: false, reason: `execution_kind "${kind}" is not auto-run by the script runner (only "script" is). Route through the normal path.`, kind };
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
const ref = args.ref || '';
|
|
3122
|
+
if (!ref) {
|
|
3123
|
+
return { ran: false, reason: 'Missing --ref (the execution_ref, for example "npm-script:test").' };
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const id = args.id || args.task || '';
|
|
3127
|
+
const title = args.title || '';
|
|
3128
|
+
const result = await runScriptTask(projectDir, { id, title, execution_ref: ref });
|
|
3129
|
+
|
|
3130
|
+
// Write a synthetic handoff so the pipeline reads it like an agent handoff.
|
|
3131
|
+
const timestamp = new Date().toISOString();
|
|
3132
|
+
const handoff = formatScriptHandoff({ id, title }, result, timestamp);
|
|
3133
|
+
let handoffPath = null;
|
|
3134
|
+
try {
|
|
3135
|
+
const dir = join(projectDir, resolveFrameworkDir(projectDir), 'artifacts', 'handoffs');
|
|
3136
|
+
mkdirSync(dir, { recursive: true });
|
|
3137
|
+
handoffPath = join(dir, `script-${(id || 'task').replace(/[^A-Za-z0-9._-]/g, '_')}.md`);
|
|
3138
|
+
writeFileSync(handoffPath, handoff, 'utf-8');
|
|
3139
|
+
} catch {
|
|
3140
|
+
// best-effort; the structured result is still returned for the orchestrator
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
return {
|
|
3144
|
+
ran: true,
|
|
3145
|
+
executor: result.executor,
|
|
3146
|
+
arg: result.arg,
|
|
3147
|
+
status: result.status,
|
|
3148
|
+
summary: result.summary,
|
|
3149
|
+
durationMs: result.durationMs,
|
|
3150
|
+
handoffPath,
|
|
3151
|
+
details: result.details,
|
|
3152
|
+
};
|
|
2985
3153
|
}
|
|
2986
3154
|
|
|
2987
3155
|
/**
|
|
@@ -3202,6 +3370,9 @@ export async function runOrchestrate(subCommand, argv, projectDir) {
|
|
|
3202
3370
|
case 'assess-coverage':
|
|
3203
3371
|
output(handleAssessCoverage(args));
|
|
3204
3372
|
break;
|
|
3373
|
+
case 'run-script-task':
|
|
3374
|
+
output(await handleRunScriptTask(projectDir, args));
|
|
3375
|
+
break;
|
|
3205
3376
|
default:
|
|
3206
3377
|
output(errorResult(`Unknown sub-command: ${subCommand}. Valid: next, advance, init, validate-handoff, status, deviation, exit, providers, detect-flow, backlog, qa-plan-score, qa-impl-score, scan, spawn-team, team-status, team-dissolve`, 'UNKNOWN_COMMAND'));
|
|
3207
3378
|
break;
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview /chati doctor — silent
|
|
2
|
+
* @fileoverview /chati doctor — silent self-diagnostic.
|
|
3
3
|
*
|
|
4
|
-
* Runs a battery of fast (< 200ms) filesystem + version checks
|
|
5
|
-
* /chati
|
|
6
|
-
* sees doctor output when everything is fine; they see a
|
|
7
|
-
* actionable message ONLY when something is off.
|
|
4
|
+
* Runs a battery of fast (< 200ms) filesystem + version checks when invoked
|
|
5
|
+
* via the /chati doctor subcommand. Pass-silent, fail-loud: the intent is that
|
|
6
|
+
* the user never sees doctor output when everything is fine; they see a
|
|
7
|
+
* specific, actionable message ONLY when something is off.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Seven checks, two severities:
|
|
10
10
|
*
|
|
11
|
-
* id
|
|
12
|
-
*
|
|
13
|
-
* license
|
|
14
|
-
* scaffold
|
|
15
|
-
* session
|
|
16
|
-
* hooks
|
|
17
|
-
* model
|
|
18
|
-
* node
|
|
11
|
+
* id severity description
|
|
12
|
+
* ------------ -------- -----------
|
|
13
|
+
* license BLOCK ~/.chati-dev/license.yaml status != VALID (fail-open if missing)
|
|
14
|
+
* scaffold BLOCK projectDir/.chati.dev/ directory exists
|
|
15
|
+
* session BLOCK .chati/session.yaml parses cleanly (or is absent)
|
|
16
|
+
* hooks WARN .claude/settings.json has expected hook registrations
|
|
17
|
+
* model WARN session.active_model is populated (hook fills on first fire)
|
|
18
|
+
* node BLOCK process version >= 22
|
|
19
|
+
* constitution WARN articleCount + i18n match the real "## Article" count
|
|
19
20
|
*
|
|
20
21
|
* BLOCK failures short-circuit boot with a fix instruction. WARN failures
|
|
21
22
|
* surface as non-blocking notices.
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
* verdict.
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
import { existsSync, readFileSync } from 'fs';
|
|
29
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
29
30
|
import { join } from 'path';
|
|
30
31
|
import { readLicenseStatus } from '../license/wait.js';
|
|
31
32
|
|
|
@@ -242,8 +243,87 @@ export function checkNode({ versionString = process.version } = {}) {
|
|
|
242
243
|
};
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Constitution metadata drift. The articleCount in domains/constitution.yaml
|
|
248
|
+
* and the per-locale i18n strings must match the real number of "## Article"
|
|
249
|
+
* headings in constitution.md. Drift here is metadata-only (runtime is
|
|
250
|
+
* unaffected), so it is a WARN. Catching it keeps the registry, the localized
|
|
251
|
+
* UI and the document in sync.
|
|
252
|
+
* @param {{ projectDir: string }} opts
|
|
253
|
+
* @returns {CheckResult}
|
|
254
|
+
*/
|
|
255
|
+
export function checkConstitution({ projectDir }) {
|
|
256
|
+
const fwDir = ['.chati.dev', 'chati.dev']
|
|
257
|
+
.map(d => join(projectDir, d))
|
|
258
|
+
.find(d => existsSync(join(d, 'constitution.md')));
|
|
259
|
+
if (!fwDir) {
|
|
260
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: 'constitution.md not found — skipping drift check.' };
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const md = readFileSync(join(fwDir, 'constitution.md'), 'utf-8');
|
|
264
|
+
const real = (md.match(/^#{2,3} Article /gm) || []).length;
|
|
265
|
+
const mismatches = [];
|
|
266
|
+
|
|
267
|
+
const domainPath = join(fwDir, 'domains', 'constitution.yaml');
|
|
268
|
+
if (existsSync(domainPath)) {
|
|
269
|
+
const m = readFileSync(domainPath, 'utf-8').match(/^articleCount:\s*(\d+)/m);
|
|
270
|
+
if (m && parseInt(m[1], 10) !== real) mismatches.push(`domains/constitution.yaml=${m[1]}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const locale of ['en', 'es', 'fr', 'pt']) {
|
|
274
|
+
const p = join(fwDir, 'i18n', `${locale}.yaml`);
|
|
275
|
+
if (!existsSync(p)) continue;
|
|
276
|
+
const m = readFileSync(p, 'utf-8').match(/constitution:\s*"[^"]*?(\d{1,3})\s*(?:Article|Articulo|Artigo)/i);
|
|
277
|
+
if (m && parseInt(m[1], 10) !== real) mismatches.push(`i18n/${locale}.yaml=${m[1]}`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (mismatches.length === 0) {
|
|
281
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: `Constitution metadata in sync (${real} Articles).` };
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
id: 'constitution',
|
|
285
|
+
severity: SEVERITY.WARN,
|
|
286
|
+
status: 'fail',
|
|
287
|
+
message: `Constitution article-count drift (real=${real}): ${mismatches.join('; ')}`,
|
|
288
|
+
fix: `Set those values to ${real} to match constitution.md.`,
|
|
289
|
+
};
|
|
290
|
+
} catch (err) {
|
|
291
|
+
return { id: 'constitution', severity: SEVERITY.WARN, status: 'pass', message: `Cannot verify constitution metadata: ${err.message}` };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Surface durable pipeline-advance failure markers. advance-trigger.js drops a
|
|
297
|
+
* JSON marker in .chati/failures/ when it cannot spawn the router (a silent
|
|
298
|
+
* pipeline stall). Re-surface them here so the stall is visible on the next
|
|
299
|
+
* doctor run instead of only living as an unread file.
|
|
300
|
+
* @param {{ projectDir: string }} opts
|
|
301
|
+
* @returns {CheckResult}
|
|
302
|
+
*/
|
|
303
|
+
export function checkFailures({ projectDir }) {
|
|
304
|
+
const failuresDir = join(projectDir, '.chati', 'failures');
|
|
305
|
+
if (!existsSync(failuresDir)) {
|
|
306
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: 'No pipeline-advance failures recorded.' };
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
const markers = readdirSync(failuresDir).filter(f => f.endsWith('.json'));
|
|
310
|
+
if (markers.length === 0) {
|
|
311
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: 'No pipeline-advance failures recorded.' };
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
id: 'failures',
|
|
315
|
+
severity: SEVERITY.WARN,
|
|
316
|
+
status: 'fail',
|
|
317
|
+
message: `${markers.length} pipeline-advance failure marker(s) in .chati/failures/ — the pipeline may have stalled silently.`,
|
|
318
|
+
fix: 'Inspect .chati/failures/*.json, resolve the cause (often node not on PATH for the hook process), then delete the markers.',
|
|
319
|
+
};
|
|
320
|
+
} catch (err) {
|
|
321
|
+
return { id: 'failures', severity: SEVERITY.WARN, status: 'pass', message: `Cannot read .chati/failures/: ${err.message}` };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
245
325
|
// ---------------------------------------------------------------------------
|
|
246
|
-
// runDoctor — aggregate all
|
|
326
|
+
// runDoctor — aggregate all checks.
|
|
247
327
|
// ---------------------------------------------------------------------------
|
|
248
328
|
|
|
249
329
|
/**
|
|
@@ -263,6 +343,8 @@ export function runDoctor(opts) {
|
|
|
263
343
|
checkHooks({ projectDir: opts.projectDir }),
|
|
264
344
|
checkModel({ projectDir: opts.projectDir }),
|
|
265
345
|
checkNode(),
|
|
346
|
+
checkConstitution({ projectDir: opts.projectDir }),
|
|
347
|
+
checkFailures({ projectDir: opts.projectDir }),
|
|
266
348
|
];
|
|
267
349
|
const blockers = checks.filter(c => c.status === 'fail' && c.severity === SEVERITY.BLOCK);
|
|
268
350
|
const warnings = checks.filter(c => c.status === 'fail' && c.severity === SEVERITY.WARN);
|
|
@@ -9,6 +9,8 @@ import { initCollector, track as telemetryTrack, flush as telemetryFlush } from
|
|
|
9
9
|
import { sendEvents } from '../telemetry/sender.js';
|
|
10
10
|
import { getTelemetryConfig, isEnabled as isTelemetryEnabled } from '../telemetry/config.js';
|
|
11
11
|
import { getCurrentVersion } from '../upgrade/checker.js';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Pipeline phases in order.
|
|
@@ -107,6 +109,35 @@ const QA_THRESHOLDS = {
|
|
|
107
109
|
'qa-visual': QA_VISUAL_THRESHOLD,
|
|
108
110
|
};
|
|
109
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Resolve the gate thresholds from the canonical rubric file
|
|
114
|
+
* (chati.dev/data/qa-rubrics.yaml), falling back to the constants above when
|
|
115
|
+
* the file is unavailable (for example in a unit test with no project on disk).
|
|
116
|
+
* The constants stay as the documented fallback, not as a second source of
|
|
117
|
+
* truth: when the rubric file is present its values win. Cached per directory.
|
|
118
|
+
*/
|
|
119
|
+
const _qaThresholdCache = new Map();
|
|
120
|
+
function loadQaThresholds(projectDir) {
|
|
121
|
+
const dir = projectDir || process.cwd();
|
|
122
|
+
if (_qaThresholdCache.has(dir)) return _qaThresholdCache.get(dir);
|
|
123
|
+
const candidates = ['.chati.dev', 'chati.dev'].map(
|
|
124
|
+
(d) => join(dir, d, 'data', 'qa-rubrics.yaml')
|
|
125
|
+
);
|
|
126
|
+
let raw = null;
|
|
127
|
+
for (const p of candidates) {
|
|
128
|
+
if (existsSync(p)) { try { raw = readFileSync(p, 'utf-8'); break; } catch { /* fall through */ } }
|
|
129
|
+
}
|
|
130
|
+
const out = { ...QA_THRESHOLDS };
|
|
131
|
+
if (raw) {
|
|
132
|
+
for (const key of Object.keys(QA_THRESHOLDS)) {
|
|
133
|
+
const m = raw.match(new RegExp(`^\\s*${key}:\\s*(\\d+)`, 'm'));
|
|
134
|
+
if (m) out[key] = parseInt(m[1], 10);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
_qaThresholdCache.set(dir, out);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
110
141
|
/**
|
|
111
142
|
* Route a failing QA to the correct rework target via Fault Vector Protocol.
|
|
112
143
|
*
|
|
@@ -180,16 +211,6 @@ function correctionKey(qaAgent, target) {
|
|
|
180
211
|
return `${qaAgent}->${target}`;
|
|
181
212
|
}
|
|
182
213
|
|
|
183
|
-
/**
|
|
184
|
-
* Check if a QA agent's score is below its threshold.
|
|
185
|
-
*/
|
|
186
|
-
function qaScoreBelowThreshold(qaAgent, score) {
|
|
187
|
-
const threshold = QA_THRESHOLDS[qaAgent];
|
|
188
|
-
if (threshold === undefined) return false;
|
|
189
|
-
if (score === null || score === undefined) return true;
|
|
190
|
-
return score < threshold;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
214
|
/**
|
|
194
215
|
* Initialize a new pipeline for a project.
|
|
195
216
|
*
|
|
@@ -690,6 +711,7 @@ function getFirstAgentInPhase(state, phase) {
|
|
|
690
711
|
*/
|
|
691
712
|
export function checkPhaseTransition(pipelineState) {
|
|
692
713
|
const { phase, agents } = pipelineState;
|
|
714
|
+
const T = loadQaThresholds(pipelineState.targetDir);
|
|
693
715
|
|
|
694
716
|
// Check based on current phase
|
|
695
717
|
if (phase === 'discover') {
|
|
@@ -731,15 +753,15 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
731
753
|
return {
|
|
732
754
|
canAdvance: false,
|
|
733
755
|
reason: 'QA-Planning not yet completed',
|
|
734
|
-
requiredScore:
|
|
756
|
+
requiredScore: T['qa-planning'],
|
|
735
757
|
};
|
|
736
758
|
}
|
|
737
759
|
|
|
738
|
-
if (qaPlanning.score === null || qaPlanning.score <
|
|
760
|
+
if (qaPlanning.score === null || qaPlanning.score < T['qa-planning']) {
|
|
739
761
|
return {
|
|
740
762
|
canAdvance: false,
|
|
741
|
-
reason: `QA-Planning score ${qaPlanning.score || 0} below threshold ${
|
|
742
|
-
requiredScore:
|
|
763
|
+
reason: `QA-Planning score ${qaPlanning.score || 0} below threshold ${T['qa-planning']}`,
|
|
764
|
+
requiredScore: T['qa-planning'],
|
|
743
765
|
};
|
|
744
766
|
}
|
|
745
767
|
|
|
@@ -795,15 +817,15 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
795
817
|
return {
|
|
796
818
|
canAdvance: false,
|
|
797
819
|
reason: 'QA-Implementation not yet completed',
|
|
798
|
-
requiredScore:
|
|
820
|
+
requiredScore: T['qa-implementation'],
|
|
799
821
|
};
|
|
800
822
|
}
|
|
801
823
|
|
|
802
|
-
if (qaImpl.score === null || qaImpl.score <
|
|
824
|
+
if (qaImpl.score === null || qaImpl.score < T['qa-implementation']) {
|
|
803
825
|
return {
|
|
804
826
|
canAdvance: false,
|
|
805
|
-
reason: `QA-Implementation score ${qaImpl.score || 0} below threshold ${
|
|
806
|
-
requiredScore:
|
|
827
|
+
reason: `QA-Implementation score ${qaImpl.score || 0} below threshold ${T['qa-implementation']}`,
|
|
828
|
+
requiredScore: T['qa-implementation'],
|
|
807
829
|
};
|
|
808
830
|
}
|
|
809
831
|
|
|
@@ -813,14 +835,14 @@ export function checkPhaseTransition(pipelineState) {
|
|
|
813
835
|
return {
|
|
814
836
|
canAdvance: false,
|
|
815
837
|
reason: 'QA-Visual not yet completed',
|
|
816
|
-
requiredScore:
|
|
838
|
+
requiredScore: T['qa-visual'],
|
|
817
839
|
};
|
|
818
840
|
}
|
|
819
|
-
if (qaVisual.score === null || qaVisual.score <
|
|
841
|
+
if (qaVisual.score === null || qaVisual.score < T['qa-visual']) {
|
|
820
842
|
return {
|
|
821
843
|
canAdvance: false,
|
|
822
|
-
reason: `QA-Visual score ${qaVisual.score || 0} below threshold
|
|
823
|
-
requiredScore:
|
|
844
|
+
reason: `QA-Visual score ${qaVisual.score || 0} below threshold ${T['qa-visual']}`,
|
|
845
|
+
requiredScore: T['qa-visual'],
|
|
824
846
|
};
|
|
825
847
|
}
|
|
826
848
|
}
|